Windows Phone / [Из песочницы] Разрабатываем «Домашний бюджет». Часть 1

в 14:43, , рубрики: silverlight, windows phone 7, разработка, метки: , , ,

Windows Phone / [Из песочницы] Разрабатываем «Домашний бюджет». Часть 1
Данная статья является первой частью потенциальной серии, в которой будет описан процесс создания приложения для удобного ведения домашнего бюджета на WP7 с самого начала. При этом в разработке будут использованы разнообразные API и возможности платформы с целью максимального их охвата. В тоже время программа будет абсолютно юзабельной (как минимум один постоянный юзер у неё будет), и мусора используемого исключительно в академических целях не будет.
Выбор типа приложения связан с тем, что первая купленная мной программа в marketplace была именно бюджетилкой но некоторых полезных функций (например автоматического бекапа на SkyDrive и т.п.) в ней нет и не планируются.
Принципы разработки

Функционал будет наращиваться постепенно, небольшими итерациями длительностью в 10-14 дней, из которых 3-5 дней выделяются на обкатку прошлой версии, 1 день на мозговой штурм а остальное на реализацию задуманного, тестирование и оформление статьи.
Так как создаваемое приложение я планирую использовать повседневно вместо аналогичной утилиты – функционалу придётся быть именно тем, который необходим, а не тем, который проще написать.
Первая итерация: план

В первую очень мы должны уметь добавлять транзакции и иметь хотя-бы базовые категории, ибо иначе за неделю использования образуется небольшая помойка, которую даже не захочется открывать.
В данной статье мы подробно (возможно даже слишком) рассмотрим процесс создания БД и процесс добавления категорий. Процесс добавления транзакций мы рассматривать не будем так как он практически на 100% аналогичен работе с категориями и его рассмотрение раздуло бы статью до уж слишком больших размеров. Также мы не рассмотрим процесс редактирования категорий, но учитывая используемую модель работы – это займёт +- 10 минут.
При необходимости эти темы можно будет мельком пробежать в следующей статье или пропустить.
Соответственно мы рассмотрим такие вопросы:
Базовые принципы разработки под WP7

Некоторые основы Metro[1]
Работа с SQLCE базой данных и создание модели по принципу code-first

Пререквизиты

Базовые знания .NET и C#

Windows Phone SDK 7.1

Silverlight for Windows Phone Toolkit (версия от ноября 2011)

Prism for Phone updated for Mango[2]

Разработка: создание БД
На случай если у кого-то возникнет желание использовать данную статью в роли урока – после каждого логически завершённого и более-менее важного этапа будет приведена ссылка на архив с результатами. Все исходники на SkyDrive. Отдельные файлы в случае сокращений будут представлены на pastebin.Выбирая структуру приложения я ориентируюсь в первую очередь на логическое разделение выполняемых функций по проектам. Исходя из этих соображений для описания БД и всех типов данных создаём отдельный проект Entities не забываем добавить в References System.Data.Linq и Microsoft.Practices.Prism. Solution тут.Сразу оговорюсь – под понятием транзакции я подразумеваю финансовую транзакцию, а не транзакцию в БД.
При создании БД мы будем использовать подход code-first[3].Для сегодняшнего задания нас устроят аж целые две таблицы – Transactions и Categories. Создадим две пустые таблицы и добавим их в БД.Transaction.cs
[Table(Name = "Transactions")]
public class Transaction : NotificationObject
Category.cs
[Table(Name = "Categories")]
public class Category : NotificationObject
Database.cs
public class Database : DataContext
{
private static string DBConnectionString = "Data Source=isostore:/Database.sdf";

public Database()
: base(Database.DBConnectionString)
{
this.DeferredLoadingEnabled = true;
}

public Table Categories;

public Table Transactions;
}
Даже не думайте делать таблицы в БД свойствами а не полями. Я из-за стилистической привычки использовать для public’а свойства убил около часа на попытки понять почему БД вообще не работает.
Здесь транзакции и категории наследуют определённый в Prism’е NotificationObject для нормального взаимодействия с UI в будущем. Кстати, мы при разработке используем паттерн MVVM.
В конструкторе БД выставляем флаг DefferedLoadingEnabled для отключения автоматической загрузки связаных объектов из БД. Нужно будет – отдельно укажем.
Приступаем к формированию содержимого таблиц. В результате у нас должно получиться что-то подобное:
Самые интересные моменты в Transaction.cs:
[Column(IsPrimaryKey = true)]
public Guid ID
{ ... }
...
private EntityRef category;

[Association(Name = "FK_Transactions_Category", Storage = "category", ThisKey = "CategoryID", IsForeignKey = true)]
public Categories.Category Category
{
get
{
return this.category.Entity;
}

set
{
Categories.Category previousValue = this.category.Entity;
if (((previousValue != value) || (this.category.HasLoadedOrAssignedValue == false)))
{
if ((previousValue != null))
{
this.category.Entity = null;
previousValue.Transactions.Remove(this);
}

this.category.Entity = value;
if ((value != null))
{
if ((value.AddedTransactions == null) || (!value.AddedTransactions.Contains(this)))
{
value.Transactions.Add(this);
}

this.CategoryID = value.ID;
}
else
{
this.category = new EntityRef();
}

this.RaisePropertyChanged(() => this.Category);
}
}
}

Параметр ID – столбец таблицы и первичный ключ. Остальные столбцы также задаются атрибутом Column. Более подробно про Attribute-based mapping можно почитать на msdn.
Category вместе с CategoryID отвечают за привязку транзакций к категориям и на этом примере мы создали внешний ключ FK_Transactions_Category. Причина раздутого сеттера – при назначении какой-то транзакции родительской категории мы должны из предыдущей родительской категории удалить транзакцию, а в новую – добавить. Грубо говоря – Navigation Property из EF. В свою очередь в Category для реализации этого используется минимум кода.Category.cs:
public EntitySet Transactions
{
get
{
if (this.transactions == null)
{
this.transactions = new EntitySet();
this.transactions.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(this.OnTransactionsChanged);
}

return this.transactions;
}
...
}
...
private void OnTransactionsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
this.AddedTransactions = e.NewItems;
foreach (Transactions.Transaction t in e.NewItems)
{
t.Category = this;
}

this.AddedTransactions = null;
this.RaisePropertyChanged(() => this.Transactions);
}

По сути, в Category мы отлавливаем ситуацию когда не транзакции назначается категория, а в список транзакций категории добавляется новый элемент.
База готова. Solution на SkyDrive.
Разработка: создаём UI

Проект Shell у нас создан как Windows Phone Application, то есть на данном этапе мы не будем использовать такие контролы как Pivot/Panorama. Взаимодействие пользователя с приложением будет происходить примерно по такой схеме:Для создания использовался Expression Blend with SketchFlow (не входит в бесплатный SDK) и шаблон sketchflow для WP7 (CodePlex)Эти экраны мы разобьём на такие View: New/Edit transaction, New/Edit category, Categories list, Transactions list, причём часть отвечающая за работу с транзакциями выносим в отдельный проект. Solution на SkyDrive.
В первую очередь нам необходимо реализовать функционал просмотра списка категорий и добавление категорий. Ничего особенного в этом нет, НО так как мы стараемся делать упор на производительности – нам нужно будет немного доработать наш Database. Дело в том, что при просмотре списка категорий мы не собираемся ничего редактировать – нам нужно просто максимально быстро получить список категорий. Для этого мы внесём такую правку в Database.cs:
public Database(bool isReadOnly = false)
: base(Database.DBConnectionString)
{
if (!this.DatabaseExists())
{
this.CreateDatabase();
}

this.DeferredLoadingEnabled = true;
this.ObjectTrackingEnabled = !isReadOnly;
}
Тем самым при isReadOnly==true мы отключаем слежение за объектами контекста на предмет их изменения, что в среднем увеличивает скорость простого чтения более чем в 10 раз.
При создании UI одна из проблем с которой мы сталкиваемся – невозможность прицепить к ApplicationBarButton какой-либо Behavior (нам это нужно для биндинга к команде). В Prism.Interactions есть DependencyProperty ApplicationBarButtonCommand но у меня почему-то не заработало. Поэтому пришлось использовать вполне себе удобную библиотеку AppBarUtils.
Интересные моменты из CategoriesView.xaml:


Чаще всего действиями кнопок будут переходы на другие страницы приложения и нам нужно сделать удобный механизм работы с навигацией из ViewModel. Удобный и относительно привычный (я когда-то работал с десктопным MVVM по подобному принципу) способ описан вот здесь. Похожий принцип мы и реализуем в нашем проекте Common создав класс ApplicationController. Также все наши View’s будут определены в статическом классе KnownPages:
public static class KnownPages
{
public const string AddCategory = "/Views/CategoryAddEditView.xaml?mode=Add";

public const string EditCategory = "/Views/CategoryAddEditView.xaml?mode=Edit&id={0}";

public const string ListCategories = "/Views/CategoriesView.xaml";

public const string CategoryDetails = "/Views/CategoryDetailsView.xaml?id={0}";
}
, a NavigateTo() из ApplicationController (таки мало осталось от оригинального) будет выглядеть так
public void NavigateTo(string url, params object[] data)
{
Uri address = new Uri(String.Format(url, data), UriKind.Relative);
PhoneApplicationFrame root = Application.Current.RootVisual as PhoneApplicationFrame;
root.Navigate(address);
}
Теперь, так как мы передаём параметр mode=Add на страничку AddEdit, нам необходимо во ViewModel отловить событие навигации и получить данные из строки. К сожалению, на данный момент я остановился на варианте переопределения метода OnNavigatedTo в CodeBehind’e и вызова соответствующего метода во ViewModel.CategoryAddEditView.xaml.cs:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
((CategoryAddEditViewModel)this.DataContext).OnNavigatedTo(this.NavigationContext, this.NavigationService.BackStack.First());
}
Как видно из кода – мы передаём не только навигационный контекст (откуда удобно выдёргивать параметры из адреса страницы), но и страничку с которой мы перешли на текущую.
Теперь пришла очередь реализовать процесс добавления категории. Обычная View и обычная ViewModel. Но есть два но. Первое – эти же M-V-VM мы будем использовать и для редактирования категорий (будет домашним заданием), соответственно из NavigationContext мы получаем и обрабатываем параметр mode. Второе – в WP7 для TextBox’а изменение значения свойства происходит только при потере фокуса элементом. Родными способами это не реализуется, поэтому для этого мы используем Prism (Файл CategoryAddEditView.xaml):
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Interactivity;assembly=Microsoft.Practices.Prism.Interactivity"
...

Сам процесс сохранения категории выглядит так:CategoryAddEditViewMode.cs
public void SaveCategory()
{
if (!this.isEditMode)
{
this.model.AddCategory(this.Category);
ApplicationController.Default.GoBack();
}
}
На что стоит обратить внимание – мы не переходим на страничку CategoriesView а выполняем возврат на предыдущую страничку. Стоит обращать внимание на подобные переходы внутри приложения для того, чтобы пользователь не оказывался в недоумении не там где он предполагал после нажатия на кнопку Назад.
В CategoryAddEditModel.cs сохранение выглядит так:
public void AddCategory(Category cat)
{
using (Database db = new Database())
{
db.Categories.InsertOnSubmit(cat);
db.SubmitChanges();
}
}
Видно что отсутствуют какие-либо проверки и валидации – и это плохо. Но для первой статьи уже довольно много материала, и нам сейчас важнее закончить основной функционал и начать пользоваться программой – остальное сделаем между статьями или в следующих.
При возвращении на список категорий View и ViewModel не пересодаются, поэтому при переход со страницы списка на страницу добавления мы выставим флаг IsReloadPending а по возвращению обработаем и сбросим его.CategoriesViewModel.cs:
private void AddCategory()
{
this.isReloadPending = true;
ApplicationController.Default.NavigateTo(KnownPages.AddCategory);
}

public void OnNavigatedTo(NavigationContext context, JournalEntry lastPage)
{
if (this.isReloadPending)
{
this.isReloadPending = false;
this.Categories = this.model.GetCategoriesList();
}
}

Итоги
За сегодня мы получили все нужные инструменты для работы с WP7, опробовали работу с БД, подготовили почву для дальнейшей разработки программы и обучения технологиями программирования для Windows Phone. Также мы столкнулись с парой косяков (ApplicationBar, TextBox) и преодолели их.
Да – мы не получили готовую для использования (просто в качестве сборщика данных) программу, но от этого этапа нас отделяет примерно 1-2 часа. Кому интересно – попробуют сами. Solution на SkyDrive.
Параллельно, те кто знаком с C# должны были понять что мобильная платформа от Microsoft довольно проста для обучения и легко может быть освоена самостоятельно.В тоже время я понял что написание подобной статьи занимает довольно много времени. Статья писалась в формате дневника параллельно с написание приложения.
Дальнейшие планы
В следующей статье я бы хотел рассмотреть:
процесс создания вторичного Tile

оптимизацию запуска приложения

прототипирование приложения в SketchFlow

Литература

WP7 UI/UX Notes

Developer's Guide to Microsoft Prism

Programming Guide (LINQ to SQL)


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js