Как и многие люди, перед которыми стоит задача написания очередного UI для своего приложения, я периодически сталкиваюсь с необходимостью создания для UI своей собственной модели, которая в какой-то мере повторяет модель предметной области, однако при этом расширяем и/или изменяет ее. И вот что из этого вышло.
Например, у нас есть класс Region такой структуры
И есть задача показать пользователю в UI дерево регионов, построенное по этой модели, а так же дать возможность выбрать определенные регионы, которые подлежат какой-то дальнейшей обработке. Если мы делаем наш UI по MVVM архитектуре, то очевидно, что нам понадобится создать класс типа RegionViewModel, со структурой, аналогичной классу Region, но имеющей дополнительное свойство IsSelected. К тому же было бы удобно, если бы у RegionViewModel свойство SubRegions возращало энумератор типа IEnumerable<RegionViewModel>, тогда весь код UI свелся бы к созданию байндига у TreeView на список RegionViewModel и функции получения всех RegionViewModel, у которых свойство IsSelected равно true. А в чем же проблема? Проблема в том, что большая часть кода класса RegionViewModel будет представлять собой проброс данных в класс Region, который он и оборачивает. Возможна, конечно, такая реализация, несколько упрощающая жизнь:
Но и она требует от нас реализовать руками свойство SubRegions. А если Region не реализует INotifyPropertyChanged, то скорее всего нам придется руками описать каждое свойство, добавив к установке значения вызов соответствующего события.
Если задача, подобная описанной выше, встречается довольно часто, то написание каждый раз вью-модели руками может утомить. Поэтому я задался вопрос автоматизации данного процесса и вот что получилось.
Если приглядеться, то становится видно, что наша задача сводиться к созданию некого прокси-генератора, который создает прокси для заданного класса и дополняет его какими-то аспектами, типа INotifyPropertyChanged, или/и добавляет новые свойства, методы и т.д. Что нам предлагает .net стек для создания проксей?
Можно выделить следующие решения
Я выбрал 5-й вариант, т.к. RealProxy потребует вмешательства в сами классы модели, прокси от кастл позволит перехватить только виртуальные члены класса, постшарп стоит денег, а кодогенерацию я просто не люблю по религиозным причинам. К тому же создание собственного велосипеда всегда интересней.
И так, основная идея заключает в следующем – описываем класс, который наследуется от типа DynamicObject и переопределяет TryInvokeMember, TrySetMember и TryGetMember.
Например
Делам так, чтобы он принимал на входе нужный объект, а при попытке вызова какого-то члена класса, прокидывал вызов в этот объект. Добавляем к этому возможность добавление новых методов и свойство. Для построение объекта будем использовать патер Fluent builder.
Сам код прокси-класс получился довольно большим(порядка 280 строчек), поэтому желающие его увидеть могут скачать исходники и все примеры по ссылке внизу поста. Здесь же я приведу примеры использования.
Так же сделана автоматическая реализация INotifyPropertyChanged
Давайте попробуем решить с помощью данного прокси задачу, которая описана в начале статьи. Для упрощения мы сгенерируем небольшое дерево регионов, дадим пользователю выбрать нужные и покажем справа список выбранных. Собственно вот так привыглядит код окна
И xaml к нему
Спасибо за внимание.
Постановка задачи
Например, у нас есть класс Region такой структуры
- public class Region
- {
- public string Name { get; set; }
- public int Index { get; set; }
- public IEnumerable<Region> SubRegions { get; set; }
- }
* This source code was highlighted with Source Code Highlighter.
И есть задача показать пользователю в UI дерево регионов, построенное по этой модели, а так же дать возможность выбрать определенные регионы, которые подлежат какой-то дальнейшей обработке. Если мы делаем наш UI по MVVM архитектуре, то очевидно, что нам понадобится создать класс типа RegionViewModel, со структурой, аналогичной классу Region, но имеющей дополнительное свойство IsSelected. К тому же было бы удобно, если бы у RegionViewModel свойство SubRegions возращало энумератор типа IEnumerable<RegionViewModel>, тогда весь код UI свелся бы к созданию байндига у TreeView на список RegionViewModel и функции получения всех RegionViewModel, у которых свойство IsSelected равно true. А в чем же проблема? Проблема в том, что большая часть кода класса RegionViewModel будет представлять собой проброс данных в класс Region, который он и оборачивает. Возможна, конечно, такая реализация, несколько упрощающая жизнь:
- public class RegionViewModel
- {
- public Region Value { get; set; }
- public IEnumerable<RegionViewModel> SubRegions { get {/*...*/ } }
- }
* This source code was highlighted with Source Code Highlighter.
Но и она требует от нас реализовать руками свойство SubRegions. А если Region не реализует INotifyPropertyChanged, то скорее всего нам придется руками описать каждое свойство, добавив к установке значения вызов соответствующего события.
Если задача, подобная описанной выше, встречается довольно часто, то написание каждый раз вью-модели руками может утомить. Поэтому я задался вопрос автоматизации данного процесса и вот что получилось.
Возможные решения
Если приглядеться, то становится видно, что наша задача сводиться к созданию некого прокси-генератора, который создает прокси для заданного класса и дополняет его какими-то аспектами, типа INotifyPropertyChanged, или/и добавляет новые свойства, методы и т.д. Что нам предлагает .net стек для создания проксей?
Можно выделить следующие решения
- DynamicProxy от Castle
- RealProxy от MS
- кодогенерацию на базе T4
- создать что-нибудь на базе инструментирования кода с помощью PostSharp.
- Использовать появившийся в C# 4.0 dynamic
Я выбрал 5-й вариант, т.к. RealProxy потребует вмешательства в сами классы модели, прокси от кастл позволит перехватить только виртуальные члены класса, постшарп стоит денег, а кодогенерацию я просто не люблю по религиозным причинам. К тому же создание собственного велосипеда всегда интересней.
Реализация
И так, основная идея заключает в следующем – описываем класс, который наследуется от типа DynamicObject и переопределяет TryInvokeMember, TrySetMember и TryGetMember.
Например
- public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
- {
- result = _methods.ContainsKey(binder.Name) ? _methods[binder.Name].DynamicInvoke(new[] { this }.Concat(args).ToArray()) : InvokeNativeMethod(binder.Name,args);
-
- result = GetResult(result);
-
- return true;
- }
* This source code was highlighted with Source Code Highlighter.
Делам так, чтобы он принимал на входе нужный объект, а при попытке вызова какого-то члена класса, прокидывал вызов в этот объект. Добавляем к этому возможность добавление новых методов и свойство. Для построение объекта будем использовать патер Fluent builder.
Сам код прокси-класс получился довольно большим(порядка 280 строчек), поэтому желающие его увидеть могут скачать исходники и все примеры по ссылке внизу поста. Здесь же я приведу примеры использования.
Добавление свойств
- private class MyClass
- {
- MyClass _i;
-
- public string Name { get; set; }
- public MyClass Foo()
- {
- return _i ?? (_i = new MyClass{Name = "_sdfdsfsdfsfd"});
- }
-
- public IEnumerable<MyClass> GetChilds()
- {
- yield return new MyClass();
- yield return new MyClass();
- }
- }
-
- [TestMethod]
- public void TestAddProperties()
- {
- var a = new MyClass{Name="123"};
-
- Assert.AreEqual("123",a.Name);
- dynamic proxy = DynamicProxy.Create(a).AddProperty<bool>("IsSelected")
- .AddProperty("X", _ => x, (_, value) => x = value)
- .AddProperty("LastName","FFFF")
-
-
- proxy.Name = "567";
- proxy.IsSelected = true;
- proxy.X = 42;
-
- Assert.AreEqual("567",a.Name);
- Assert.IsTrue(proxy.IsSelected);
- Assert.AreEqual(42, x);
-
- proxy.IsSelected = false;
-
- Assert.IsFalse(proxy.IsSelected);
- }
* This source code was highlighted with Source Code Highlighter.
Автосоздание прокси для значений функций и свойств
- [TestMethod]
- public void TestChilds()
- {
- var a = new MyClass{Name="123"};
-
- Assert.AreEqual("123",a.Name);
- Assert.AreEqual("_sdfdsfsdfsfd", a.Foo().Name);
-
- var x = 0;
-
- dynamic proxy = DynamicProxy.Create(a).AddProperty<bool>("IsSelected")
- .AddProperty("X", _ => x, (_, value) => x = value)
- .AddProperty("LastName","FFFF")
- .AddMethod("Boo", new Func<DynamicProxy<MyClass>, int, string>((m, i) => ((MyClass)m).Name + i.ToString()));
-
-
- proxy.Name = "567";
- proxy.IsSelected = true;
- proxy.X = 42;
- var b = proxy.Foo();
- b.IsSelected = true;
-
- Assert.AreEqual("567",a.Name);
- Assert.AreEqual("5674",proxy.Boo(4));
- Assert.IsTrue(proxy.IsSelected);
- Assert.AreEqual(42, x);
- Assert.IsTrue(b.IsSelected);
-
- b.IsSelected = false;
-
- Assert.IsTrue(proxy.IsSelected);
- Assert.IsFalse(b.IsSelected);
-
- proxy.LastName = "890";
- var d = proxy.Foo();
- Assert.AreEqual("FFFF",d.LastName);
-
- var d2 = proxy.Foo();
- d2.LastName = "RRRRR";
-
- Assert.AreEqual("567",Foo(proxy));
-
- Assert.AreEqual(d.LastName,d2.LastName);
-
- //Можно
грабить корованы делать импицитный каст
-
- var c = (MyClass)proxy;
-
- Assert.AreEqual("567",c.Name);
-
- foreach (var child in proxy.GetChilds())
- {
- child.IsSelected = true;
- Assert.IsTrue(child.IsSelected);
- }
- }
* This source code was highlighted with Source Code Highlighter.
Так же сделана автоматическая реализация INotifyPropertyChanged
- [TestMethod]
- public void TestPropertyChange()
- {
- var myClass = new MyClass();
- var propertyName = string.Empty;
- dynamic proxy = DynamicProxy.Create(myClass);
-
- ((INotifyPropertyChanged) proxy).PropertyChanged += (s, a) => propertyName = a.PropertyName;
-
- proxy.Name = "aaaa";
-
- Assert.AreEqual("Name",propertyName);
- }
* This source code was highlighted with Source Code Highlighter.
Практическое применение
Давайте попробуем решить с помощью данного прокси задачу, которая описана в начале статьи. Для упрощения мы сгенерируем небольшое дерево регионов, дадим пользователю выбрать нужные и покажем справа список выбранных. Собственно вот так привыглядит код окна
- public MainWindow()
- {
- InitializeComponent();
- DataContext = this;
- Items = new[] { DynamicProxy.Create(CreateRegions().First()).AddProperty<bool>("IsSelected") };
- }
-
- IEnumerable<Region> GetSelectedItems(IEnumerable<dynamic> items)
- {
- return items.Where(x => x.IsSelected).Concat(items.SelectMany(x => GetSelectedItems((IEnumerable<dynamic>)x.SubRegions))).Select(x=>(Region)x);
- }
-
- private void ButtonClick(object sender, RoutedEventArgs e)
- {
- var res = GetSelectedItems(Items).Take(10).ToList();
- SelectedItems = res;
- }
* This source code was highlighted with Source Code Highlighter.
И xaml к нему
- <Window x:Class="WpfApplication1.MainWindow"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication1="clr-namespace:WpfApplication1"
- Title="MainWindow" Height="350" Width="525">
-
- <Window.Resources>
-
- <DataTemplate DataType="{x:Type WpfApplication1:Region}">
- <WrapPanel>
- <TextBlock Text="{Binding Path=Name,StringFormat='{}{0}, '}"/>
- <TextBlock Text="{Binding Path=Index}"/>
- </WrapPanel>
- </DataTemplate>
-
-
- </Window.Resources>
- <Grid>
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="*"/>
- <ColumnDefinition Width="auto"/>
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
-
- <TreeView Grid.Column="0" ItemsSource="{Binding Items}" BorderThickness="0">
- <TreeView.ItemTemplate>
- <HierarchicalDataTemplate ItemsSource="{Binding SubRegions}">
- <CheckBox IsChecked="{Binding IsSelected,Mode=TwoWay}" Content="{Binding Value}" />
-
- </HierarchicalDataTemplate>
- </TreeView.ItemTemplate>
- </TreeView>
- <Button Content="Show selected" VerticalAlignment="Center" Grid.Column="1" Click="ButtonClick"/>
- <ListBox Grid.Column="2" ItemsSource="{Binding SelectedItems}" BorderThickness="0" />
- </Grid>
- </Window>
* This source code was highlighted with Source Code Highlighter.
Скриншот приложения
Ссылки
Спасибо за внимание.