Aero Framework — передовая библиотека для промышленной и индивидуальной разработки кросс-платформенных XAML-ориентированных приложений с применением концепций MVVM-проектирования. Её основные достоинства — интуитивная ясность, предельная лаконичность, минималистичность и высокое быстродействие.

С версии 2.0 она стала платной для коммерческого использования, но остаётся свободной для учебных целей и проектов с открытым исходным кодом. Стоимость лицензии составляет 30$ на одного разработчика.

Вложив небольшую сумму денег и день-два в углубленное изучение её особенностей, вы сохраните недели и месяцы труда на реальных проектах. Более того, даже если вы опытный разработчик, значительно повысите свой технический уровень, архитектурные навыки и системность мышления. В данный момент всё зависит лишь от вашей усидчивости и трудолюбия. Гарантирую, что оно того стоит.

image

Первым делом нужно загрузить библиотеку с официального сайта или альтернативных источников:
сайт,
dropbox,
onedrive,
google.drive, — после чего распаковать архив и открыть тестовые проекты HelloAero, BankOnline, Sparrow, — они помогут быстрее войти в курс дела и послужат живыми примерами.

Следует отметить, что библиотека достаточно легко внедряется в уже существующие решения, поэтому буквально сразу может быть использована в деле. Итак, рассмотрим что же в ней примечательного на первый взгляд.

1) Эвокаторы Свойств и Команд (Property and Command Evocators)

Чтобы подписаться на событие уведомления об изменении значения свойства вместо классической конструкции

PropertyChanged += (o, args) =>
{
	if (args.PropertyName == "Text") { ... };
}

достаточно использовать более лаконичный вариант на основе лямбда выражений

this[() => Text].PropertyChanged += (o, args) => { ... };

Парсинг выражения выполняется лишь единожды при инициализации, поэтому на производительность это не оказывает существенного влияния, но делает код намного более линейным и читаемым.

Обработчики для маршрутизируемых и контекстных команд задаются схожим образом.

this[MediaCommands.Play].CanExecute += (o, args) => args.CanExecute = /* conditions */;
this[MediaCommands.Play].Executed += (o, args) => { ... };

this[Context.Make].CanExecute += (o, args) => args.CanExecute = /* conditions */;
this[Context.Make].Executed += (o, args) => { ... };

Контекстные команды — кросс-платформенная и безопасная от утечек памяти реализация команд, отдалённо напоминающая логику работы маршрутизируемых команд в WPF.

2) Равноправные Инжекции путём Экспанирования (Equitable Injections via Exposable Way)

Классические инжекции зависимостей в конструктор вью-моделей не самая лучшая практика. Да, существует множество контейнеров, реализующих такую функциональность и распространён такой подход повсеместно, но он обладает целым рядом недостатков и накладывает большие ограничения на проектируемую систему. Об этом можно долго говорить, поэтому сейчас обозначим лишь суть проблемы: инжекции в конструктор создают иерархические зависимости, то есть при возникновении необходимости добраться из низкоуровневой вью-модели к высокоуровневой без сомнительных решений нельзя. Однако существует способ прямых инжекции вью-моделей без создания иерархических зависимостей, сохраняющий их равноправность! Это очень важное свойство для сериализации и десереализации замкнутых графов объектов, что оставляет возможность для полного сохранения и восстановления логического и визуалного состояний всего приложения при его перезагрузке.

Реализуется подобное очень просто и естественно. Посмотрите пример кода в проекте HelloAero в разделе Exposable.

Пример кода
    [DataContract]
    public class GuyViewModel : ContextObject, IExposable
    {
        [DataMember]
        public int Kisses
        {
            get { return Get(() => Kisses); }
            set { Set(() => Kisses, value); }
        }

        public void Expose()
        {
            var girlViewModel = Store.Get<GirlViewModel>();

            this[() => Kisses].PropertyChanged += (sender, args) =>
            {
                Context.Get("KissGirl").RaiseCanExecuteChanged();
                Context.Get("KissGuy").RaiseCanExecuteChanged();
            };

            this[Context.Get("KissGirl")].CanExecute += (sender, args) => 
                args.CanExecute = Kisses > girlViewModel.Kisses - 2;

            this[Context.Get("KissGirl")].Executed += (sender, args) => 
                girlViewModel.Kisses++;
        }
    }

    [DataContract]
    public class GirlViewModel : ContextObject, IExposable
    {
        [DataMember]
        public int Kisses
        {
            get { return Get(() => Kisses); }
            set { Set(() => Kisses, value); }
        }

        public void Expose()
        {
            var guyViewModel = Store.Get<GuyViewModel>();

            this[() => Kisses].PropertyChanged += (sender, args) =>
            {
                Context.Get("KissGirl").RaiseCanExecuteChanged();
                Context.Get("KissGuy").RaiseCanExecuteChanged();
            };

            this[Context.Get("KissGuy")].CanExecute += (sender, args) =>
                args.CanExecute = Kisses > guyViewModel.Kisses - 3;

            this[Context.Get("KissGuy")].Executed += (sender, args) =>
                guyViewModel.Kisses++;
        }
    }



3) Умное Состояние и Умные Свойства (Smart State & Smart Properties)

В языке JavaScript реализована очень мощная концепция слаботипизированных свойств, а также встроенные возможности простой сериализации и десериализации объектов, что делает его незаменимым при разработке сложных динамических интерфейсов. Отчасти эти концепции нашли отражение в механизмах Smart State и Smart Properties.

Взгляните, насколько легко и естественно сохраняются размеры и положения окон в примерах проектов к библиотеке, при этом вью-модели кросс-платформенны и полностью чисты от интерфейсной логики.

Обратите внимание на следующие конструкции

    WindowStyle="{Smart 'WindowStyle, SingleBorderWindow'}"
    ResizeMode="{Smart 'ResizeMode, CanResizeWithGrip'}"
    Height="{Smart 'Height, 600'}" 
    Width="{Smart 'Width, 800'}"


4) Встроенные и Композитные Конвертеры (Inline & Composite Converters)

Порой встречается необходимость доступа из конвертера к представлению или его контексту данных. Для этих целей можно применить паттерн IInlineConverter

    public class ConverterEventArgs : EventArgs
    {
        public object ConvertedValue { get; set; }
        public object Value { get; private set; }
        public Type TargetType { get; private set; }
        public object Parameter { get; private set; }
        public CultureInfo Culture { get; private set; }

        public ConverterEventArgs(object value, Type targetType, object parameter, CultureInfo culture)
        {
            TargetType = targetType;
            Parameter = parameter;
            Culture = culture;
            Value = value;
        }
    }

    public interface IInlineConverter : IValueConverter
    {
        event EventHandler<ConverterEventArgs> Converting;
        event EventHandler<ConverterEventArgs> ConvertingBack;
    }

Суть метода в том, что вместо создания нового класса для конвертера мы просто перемещаем методы Convert и ConvertBack в Code Behind представления в качестве обработчиков события Converting и ConvertingBack. Это даёт полный доступ к представлению, где применяется данный экземпляр конвертера. Пример использования легко отыскать в исходных кодах.

Паттерн ICompositeConverter позволяет соединять конвертеры в цепочки, что помогает иногда избежать создания новых классов-конвертеров. Как его применять, следует посмотреть в исходных кодах.

    public interface ICompositeConverter : IValueConverter
    {
        IValueConverter PostConverter { get; set; }
        object PostConverterParameter { get; set; }
    }


5) Стеллажи для Grid (Rack for Grid)

Здесь достаточно взглянуть на код, чтобы всё стало понятно.

<Grid Rack.Rows="* 20\Auto * 2* */100 * *" Rack.Columns="* 50\*/100 *">
    <!--...-->
</Grid>

equals

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition MinHeight="20" Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="*" MaxHeight="100"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition MinWidth="100" Width="*" MaxWidth="300"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <!--...-->
</Grid>


Пожалуй, для данной статьи достаточно, она носит ознакомительный обзорный характер. Мы рассмотрели далеко не все возможности библиотеки, за кадром остались локализация, валидация данных, расширения привязки, другие типы конвертеров, синтаксический сахар и много других мелочей. При наличии интереса к материалам будут и другие статьи, поэтому не оставайтесь равнодушны, вливайтесь в обсуждения, выражайте свои отзывы в комментариях или по е-мэил, делитесь с другими.

P.S. По ссылке немного устаревшие, но всё ещё актуальные материалы
Вызвала ли у вас интерес библиотека?

Проголосовал 101 человек. Воздержалось 42 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (21)


  1. Zagrebelion
    03.10.2015 08:35
    +3

    Последний пример (со слешами в разные стороны) — ад какой-то. Если в Grid.RowDefenitions можно расставить комментарии, чтобы объяснить какой RowDefinition что означает, то вот эта строка из звёздочек, пробелов и палочек — мрак.


    1. Makeman
      03.10.2015 14:36

      Слеши нужны для определения минимальных и максимальных размеров колонок и строк. Во многих случаях можно обойтись и без них. Думаю, это не столь сложная логическая задача, понять принцип их действия. Зато при таком способе очень легко изменять сетку ячеек даже во время работы приложения, что можно увидеть в примере HelloAero.
      Спасибо за замечание!


    1. HomoLuden
      06.10.2015 14:00
      +1

      Я бы сказал то описание строк не страшнее описания геометрии Path объекта. Разметки размер сокращает сильно.
      «Нужно понять и простить» принять.


  1. lair
    03.10.2015 09:45
    +5

    Классические инжекции зависимостей в конструктор не самая лучшая практика.

    Вы, наверное, забыли добавить ограничения? Например «во вью-модели в MVVM»? Потому что в общем случае ваш тезис, скажем так, спорен.

    Да, существует множество Unity-контейнеров, реализующих такую функциональность

    Каких-каких контейнеров?


    1. Makeman
      03.10.2015 14:41

      Спасибо за замечания! Исправил спорные моменты. Да, инжекции в конструктор вью-моделей не лучший вариант.


  1. qw1
    03.10.2015 10:26
    +5

    Новое дыхание WPF. Поднимаемся выше MVVM

    Название статьи вводит в заблуждение.
    Ожидаешь увидеть нечто концептуально новое, а получаешь килограм синтаксического сахара и ведро костылей, которые после пары лет разработки в WPF каждый сам себе пишет в том или ином виде.


    1. Makeman
      03.10.2015 14:23
      -5

      Сейчас у вас поверхностное видение библиотеки и это нормально, чтобы начать различать концептуальные моменты следует применить немного больше усилий для изучения.


      1. qw1
        03.10.2015 15:02
        +2

        То есть, библиотека на самом деле революционная, а проблема в статье, которая не смогла этого раскрыть?


        1. Makeman
          03.10.2015 15:52
          -3

          На самом деле существует уже обширный ряд статей, которые более детально раскрывают новаторские идеи, применяемы ещё в предыдущей версии библиотеки. Кроме того, есть незнакомые мне люди, которых заинтересовали описываемые подходы и сама библиотека. Ознакомиться с другими статьями можете и вы по этой ссылке.

          Уверен, что при внимательном их изучении вы откроете для себя достаточно нового в области XAML-ориентированной разработки.


    1. Makeman
      03.10.2015 16:18

      Ожидаешь увидеть нечто концептуально новое, а получаешь килограм синтаксического сахара и ведро костылей, которые после пары лет разработки в WPF каждый сам себе пишет в том или ином виде.

      Скорее библиотека представляет далеко не ведро костылей, а аккуратный набор инструментов, который в умелых руках мастера творит чудеса. Является она итогом далеко не двух, а шести лет опыта активной работы над различными, успешно завершёнными, проектами, в том числе разрабатываемыми с нуля.


  1. kekekeks
    03.10.2015 21:47
    +6

    кросс-платформенных XAML-ориентированных приложений
    Список платформ не нашёл нигде.


    1. Makeman
      03.10.2015 22:45
      -5

      В папке Foundation есть файлик FYI.txt

      Полностью поддерживаются:
      • Windows Desktop (WPF)
      • Windows Phone 7, 8 (Silverlight based)

      Поддерживаются с небольшими ограничениями:
      • Windows Store (RT)
      • Universal Apps
      • Xamarin


  1. HomoLuden
    05.10.2015 17:12
    +1

    this[() => Text].PropertyChanged += (o, args) => {… };


    Сразу вопрос встает: «А отписываться как?»
    И второй следом: «А не будет ли память утекать, если не отписаться?»


    1. Makeman
      05.10.2015 19:36

      Логичный вопрос. Обычно в реальной жизни необходимости отписки не возникает, но если всё-таки нужно, то лямбда-выражение заменяем классическим методом и отписываемся, как и всегда.

      this[() => Text].PropertyChanged += OnTextChanged;
      //...
      this[() => Text].PropertyChanged -= OnTextChanged;
      

      Если подписка выполняется внутри объекта this, то никаких утечек не произойдёт, поскольку всё замкнуто на сам объект.
      При внешней подписке нужно предусмотреть отписку или ожидать момента, когда слушатель будет собран сборщиком мусора уже вместе с нашим объектом одновременно.


      1. HomoLuden
        06.10.2015 13:55

        Подписываться на PropertyChanged внутри самого INotify… инстанса я не представляю зачем. А снаружи правильнее реализовать WeakEventManager с подпиской на конкретное свойство. Тогда и отписка необязательна. Поэтому неуверен, что описанная фича вообще будет востребована.


        1. Makeman
          06.10.2015 20:57

          Подписываться на PropertyChanged внутри самого INotify… инстанса я не представляю зачем.
          Почему нет? Взгляните на примеры, это распространённая и удобная практика. Например, при изменении свойства нужно выполнить какие-либо действия внутри вью-модели. Конечно, можно поместить вызов нужной логики в сеттер свойства, но это не очень красиво.

          А снаружи правильнее реализовать WeakEventManager с подпиской на конкретное свойство. Тогда и отписка необязательна.
          На текущий момент ни в одном проекте, где применялась библиотека, реальной необходимости в слабых подписках на PropertyChanged не возникало, поэтому сейчас всё реализовано классическим образом с упором на хорошую производительность. Но ничто не мешает использовать другой подход, ведь исходный код открыт.

          Поэтому неуверен, что описанная фича вообще будет востребована.
          В тех проектах, где использовалась библиотека, эта фича была востребована :)

          Спасибо, что высказываете своё мнение!


          1. HomoLuden
            07.10.2015 13:23

            На текущий момент ни в одном проекте, где применялась библиотека, реальной необходимости в слабых подписках на PropertyChanged не возникало, поэтому сейчас всё реализовано классическим образом с упором на хорошую производительность.

            1. Приложения профилировали на тему утечек памяти? Как минимум инстансы ViewModel'ей легко могут утекать в случае, когда и если подписка делается снаружи. Памяти VM'ки потребляют немного сами по себе, но могут висеть в памяти и производить паразитные вызовы обработчиков событий и CanExecute у команд, например. Особенно доставляет хлопот дефолтный ComandManager WPF, который дергает CanExcute'ы после выгрузки контролов с UI. Стоит только поместить в CanExecute проверку состояния каких-то моделей и прочего стафа в бэке, и начинаются проблемы.
            2. Опять же по производительности разница между Weak и класической подписками может быть несущественной. Безопасность важнее.

            Почему нет? Взгляните на примеры, это распространённая и удобная практика. Например, при изменении свойства нужно выполнить какие-либо действия внутри вью-модели. Конечно, можно поместить вызов нужной логики в сеттер свойства, но это не очень красиво.

            О да! Видел я примеры такого в реальных проектах. Вместо вызова метода UpdateЧеТоТам(value) в сеттере делается подписка на PropertyChanged + портянка if… else… в обработчике события с проверкой имени свойства.
            Даже описанный в статье способ подразумевает, что в точке входа в VM (.ctor ??) размещается цепочка подписок на изменение конкретных свойств. И тогда либо опять создается по одному обработчику на свойство (суть приватный метод, который легко можно вызвать и из сеттера), либо конструктор распухает лямбдами, либо создается один метод типа SubscribePropertyChangedEvents также распухающий лямбдами.


            1. Makeman
              07.10.2015 22:04
              +1

              Приложения профилировали на тему утечек памяти?
              Конечно! В библиотеке много внимания этому уделено. Например, реализация контекстных команд (Context Commands) использует механизм слабых привязок на событие CanExecuteChanged, ведь это распространённая проблема, когда статическая вью-модель удерживает контролы, подписавшиеся на это событие, от сборки мусора.

              Также динамическая локализация реализована путём расширения привязок (Binding Extensions), потому что привязка — это медиатор, который не удерживает от сборки мусора ни представление, ни вью-модель, но на это мало обращается внимания, хотя момент очень важный.

              Насчёт подписок на PropertyChanged, да, точка входа (как правило метод Expose [это как Dispose, только наоборот]) «распухает лямбдами», но это намного удобнее — контролировать подписки и вызовы в одном месте, чем искать в разбросанном виде по самим свойствам, а также устанавливать очерёдность вызовов.


              1. HomoLuden
                09.10.2015 13:47

                Насчёт подписок на PropertyChanged, да, точка входа (как правило метод Expose [это как Dispose, только наоборот]) «распухает лямбдами», но это намного удобнее — контролировать подписки и вызовы в одном месте, чем искать в разбросанном виде по самим свойствам, а также устанавливать очерёдность вызовов.


                1. Читабельность кода с случае с длинной последовательностью лямбд будет хуже, чем у последовательности методов той же длины. Хотя это дело конвенций на проекте.
                2. Можно сгрупировать методы, вызываемые в сеттерах в один #region. Можно сгруппировать методы еще и конвенцией наименования («если нужен метод от такого-то свойства, то искать методо вида SomePropPostSet(...)»). С решарпером легко будет набрать 4-5 заглавные буквы, и метод найден.
                3. Не понял как простыня из лямбд в Expose решает проблему определения последовательности вызовов до запуска приложения. В рантайме в обоих подходах последовательность вызовов будет определяться по Call Stack. В случае с подписками, как указано в статье, Call Stack будет содержать промежуточные записи с активацией event PropertyChanged, если правильно понял логику.


                1. Makeman
                  16.10.2015 17:42

                  1. Лямбда выражения просто заменить на методы, поэтому читабельность кода из-за этого вряд ли сильно ухудшится. Во многих же случаях, лямбда-выражение описывает нужную функциональность одной-двумя строками кода, тогда как аналогичный метод занимает при стандартном форматировании минимум пять строк. Эта проблема отчасти решена в C# 6.

                  2. Можно, конечно. Всегда существуют альтернативные решения. Но в плане соотношения лаконичности и читаемости кода, на мой личный взгляд, предложенный в статье подход очень даже хорош.

                  3. Например, в методе-обработчике события PropertyChanged есть if- или switch-конструкции, которые в зависимости от имени свойства выполняют ту или иную логику. Вполне может так случиться, что при изменении первого свойства, нужно поменять второе, из-за чего мы, например, рекурсивно зайдём в этот же обработчик, что не слишком удобно для дебага.


  1. Makeman
    07.10.2015 22:04

    -