Как стать автором
Обновить

Разрабатываем «Домашний бюджет». Часть 1

Время на прочтение 9 мин
Количество просмотров 4.7K
Данная статья является первой частью потенциальной серии, в которой будет описан процесс создания приложения для удобного ведения домашнего бюджета на WP7 с самого начала. При этом в разработке будут использованы разнообразные API и возможности платформы с целью максимального их охвата. В тоже время программа будет абсолютно юзабельной (как минимум один постоянный юзер у неё будет), и мусора используемого исключительно в академических целях не будет.

Выбор типа приложения связан с тем, что первая купленная мной программа в marketplace была именно бюджетилкой но некоторых полезных функций (например автоматического бекапа на SkyDrive и т.п.) в ней нет и не планируются.

Принципы разработки

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

Так как создаваемое приложение я планирую использовать повседневно вместо аналогичной утилиты – функционалу придётся быть именно тем, который необходим, а не тем, который проще написать.

Первая итерация: план

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

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

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

Разработка: создание БД

На случай если у кого-то возникнет желание использовать данную статью в роли урока – после каждого логически завершённого и более-менее важного этапа будет приведена ссылка на архив с результатами. Все исходники на 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<Bick.Budget.Entities.Categories.Category> Categories;

    public Table<Bick.Budget.Entities.Transactions.Transaction> Transactions;
}
Даже не думайте делать таблицы в БД свойствами а не полями. Я из-за стилистической привычки использовать для public’а свойства убил около часа на попытки понять почему БД вообще не работает.
Здесь транзакции и категории наследуют определённый в Prism’е NotificationObject для нормального взаимодействия с UI в будущем. Кстати, мы при разработке используем паттерн MVVM.
В конструкторе БД выставляем флаг DefferedLoadingEnabled для отключения автоматической загрузки связаных объектов из БД. Нужно будет – отдельно укажем.
Приступаем к формированию содержимого таблиц. В результате у нас должно получиться что-то подобное:

Самые интересные моменты в Transaction.cs:
[Column(IsPrimaryKey = true)]
public Guid ID
{ ... }
...
private EntityRef<Categories.Category> 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<Categories.Category>();
            }

            this.RaisePropertyChanged(() => this.Category);
        }
    }
}
Параметр ID – столбец таблицы и первичный ключ. Остальные столбцы также задаются атрибутом Column. Более подробно про Attribute-based mapping можно почитать на msdn.
Category вместе с CategoryID отвечают за привязку транзакций к категориям и на этом примере мы создали внешний ключ FK_Transactions_Category. Причина раздутого сеттера – при назначении какой-то транзакции родительской категории мы должны из предыдущей родительской категории удалить транзакцию, а в новую – добавить. Грубо говоря – Navigation Property из EF. В свою очередь в Category для реализации этого используется минимум кода.

Category.cs:
public EntitySet<Transactions.Transaction> Transactions
{
    get
    {
        if (this.transactions == null)
        {
            this.transactions = new EntitySet<Transactions.Transaction>();
            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:
<phone:PhoneApplicationPage
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:appbar="clr-namespace:AppBarUtils;assembly=AppBarUtils">
    … 
    <phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False" Mode="Default">
            <shell:ApplicationBarIconButton IconUri="/icons/appbar.add.rest.png" Text="add new"/>
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>
    <i:Interaction.Behaviors>
        <appbar:AppBarItemCommand Id="add new" Command="{Binding Path=AddCategoryCommand}"/>
    </i:Interaction.Behaviors>
Чаще всего действиями кнопок будут переходы на другие страницы приложения и нам нужно сделать удобный механизм работы с навигацией из 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"
...
<TextBox
    TextWrapping="Wrap"
    Text="{Binding Path=Category.Name, Mode=TwoWay}">
    <i:Interaction.Behaviors>
        <prism:UpdateTextBindingOnPropertyChanged />
    </i:Interaction.Behaviors>
</TextBox>
Сам процесс сохранения категории выглядит так:
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

Литература
  1. WP7 UI/UX Notes
  2. Developer's Guide to Microsoft Prism
  3. Programming Guide (LINQ to SQL)
Теги:
Хабы:
+3
Комментарии 10
Комментарии Комментарии 10

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн