Pull to refresh

Создание динамического прокси-объекта с помощью dynamic типа

Reading time 9 min
Views 6.2K
Как и многие люди, перед которыми стоит задача написания очередного UI для своего приложения, я периодически сталкиваюсь с необходимостью создания для UI своей собственной модели, которая в какой-то мере повторяет модель предметной области, однако при этом расширяем и/или изменяет ее. И вот что из этого вышло.

Постановка задачи


Например, у нас есть класс Region такой структуры
  1. public class Region
  2. {
  3.   public string Name { get; set; }
  4.   public int Index { get; set; }
  5.   public IEnumerable<Region> SubRegions { get; set; }
  6. }
* 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, который он и оборачивает. Возможна, конечно, такая реализация, несколько упрощающая жизнь:
  1. public class RegionViewModel
  2. {
  3.   public Region Value { get; set; }
  4.   public IEnumerable<RegionViewModel> SubRegions { get {/*...*/ }  }
  5. }
* 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.
Например
  1. public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
  2.   {
  3.     result = _methods.ContainsKey(binder.Name) ? _methods[binder.Name].DynamicInvoke(new[] { this }.Concat(args).ToArray()) : InvokeNativeMethod(binder.Name,args);
  4.  
  5.     result = GetResult(result);
  6.  
  7.     return true;
  8.   }
* This source code was highlighted with Source Code Highlighter.

Делам так, чтобы он принимал на входе нужный объект, а при попытке вызова какого-то члена класса, прокидывал вызов в этот объект. Добавляем к этому возможность добавление новых методов и свойство. Для построение объекта будем использовать патер Fluent builder.

Сам код прокси-класс получился довольно большим(порядка 280 строчек), поэтому желающие его увидеть могут скачать исходники и все примеры по ссылке внизу поста. Здесь же я приведу примеры использования.

Добавление свойств


  1. private class MyClass
  2. {
  3.   MyClass _i;
  4.     
  5.   public string Name { get; set; }
  6.   public MyClass Foo()
  7.   {
  8.   return _i ?? (_i = new MyClass{Name = "_sdfdsfsdfsfd"});
  9.   }
  10.  
  11.   public IEnumerable<MyClass> GetChilds()
  12.   {
  13.     yield return new MyClass();
  14.     yield return new MyClass();
  15.   }
  16. }
  17.  
  18. [TestMethod]
  19. public void TestAddProperties()
  20. {
  21.   var a = new MyClass{Name="123"};
  22.  
  23.   Assert.AreEqual("123",a.Name);
  24.   dynamic proxy = DynamicProxy.Create(a).AddProperty<bool>("IsSelected")             
  25.                                         .AddProperty("X", _ => x, (_, value) => x = value)              
  26.                                         .AddProperty("LastName","FFFF")
  27.                                           
  28.  
  29.   proxy.Name = "567";
  30.   proxy.IsSelected = true;
  31.   proxy.X = 42;
  32.  
  33.   Assert.AreEqual("567",a.Name);
  34.   Assert.IsTrue(proxy.IsSelected);
  35.   Assert.AreEqual(42, x);
  36.  
  37.   proxy.IsSelected = false;
  38.  
  39.   Assert.IsFalse(proxy.IsSelected);
  40. }
* This source code was highlighted with Source Code Highlighter.

Автосоздание прокси для значений функций и свойств


  1. [TestMethod]
  2. public void TestChilds()
  3. {
  4.   var a = new MyClass{Name="123"};
  5.  
  6.   Assert.AreEqual("123",a.Name);
  7.   Assert.AreEqual("_sdfdsfsdfsfd", a.Foo().Name);
  8.  
  9.   var x = 0;
  10.  
  11.   dynamic proxy = DynamicProxy.Create(a).AddProperty<bool>("IsSelected")
  12.                                         .AddProperty("X", _ => x, (_, value) => x = value)
  13.                                         .AddProperty("LastName","FFFF")
  14.                                         .AddMethod("Boo", new Func<DynamicProxy<MyClass>, int, string>((m, i) => ((MyClass)m).Name + i.ToString()));
  15.                         
  16.  
  17.   proxy.Name = "567";
  18.   proxy.IsSelected = true;
  19.   proxy.X = 42;
  20.   var b = proxy.Foo();
  21.   b.IsSelected = true;
  22.  
  23.   Assert.AreEqual("567",a.Name);
  24.   Assert.AreEqual("5674",proxy.Boo(4));
  25.   Assert.IsTrue(proxy.IsSelected);
  26.   Assert.AreEqual(42, x);
  27.   Assert.IsTrue(b.IsSelected);
  28.  
  29.   b.IsSelected = false;
  30.  
  31.   Assert.IsTrue(proxy.IsSelected);
  32.   Assert.IsFalse(b.IsSelected);
  33.  
  34.   proxy.LastName = "890";
  35.   var d = proxy.Foo();
  36.   Assert.AreEqual("FFFF",d.LastName);
  37.  
  38.   var d2 = proxy.Foo();
  39.   d2.LastName = "RRRRR";
  40.  
  41.   Assert.AreEqual("567",Foo(proxy));
  42.  
  43.   Assert.AreEqual(d.LastName,d2.LastName);
  44.  
  45. //Можно грабить корованы делать импицитный каст
  46.  
  47.   var c = (MyClass)proxy;
  48.  
  49.   Assert.AreEqual("567",c.Name);
  50.  
  51.   foreach (var child in proxy.GetChilds())
  52.   {
  53.     child.IsSelected = true;
  54.     Assert.IsTrue(child.IsSelected);
  55.   }
  56. }
* This source code was highlighted with Source Code Highlighter.

Так же сделана автоматическая реализация INotifyPropertyChanged
  1. [TestMethod]
  2. public void TestPropertyChange()
  3. {
  4.   var myClass = new MyClass();
  5.   var propertyName = string.Empty;
  6.   dynamic proxy = DynamicProxy.Create(myClass);
  7.  
  8.   ((INotifyPropertyChanged) proxy).PropertyChanged += (s, a) => propertyName = a.PropertyName;
  9.  
  10.   proxy.Name = "aaaa";
  11.  
  12.   Assert.AreEqual("Name",propertyName);
  13. }
* This source code was highlighted with Source Code Highlighter.

Практическое применение


Давайте попробуем решить с помощью данного прокси задачу, которая описана в начале статьи. Для упрощения мы сгенерируем небольшое дерево регионов, дадим пользователю выбрать нужные и покажем справа список выбранных. Собственно вот так привыглядит код окна
  1. public MainWindow()
  2. {
  3.   InitializeComponent();
  4.   DataContext = this;
  5.   Items = new[] { DynamicProxy.Create(CreateRegions().First()).AddProperty<bool>("IsSelected") };
  6. }
  7.  
  8. IEnumerable<Region> GetSelectedItems(IEnumerable<dynamic> items)
  9. {
  10.   return items.Where(x => x.IsSelected).Concat(items.SelectMany(x => GetSelectedItems((IEnumerable<dynamic>)x.SubRegions))).Select(x=>(Region)x);
  11. }
  12.  
  13. private void ButtonClick(object sender, RoutedEventArgs e)
  14. {
  15.   var res = GetSelectedItems(Items).Take(10).ToList();
  16.   SelectedItems = res;
  17. }
* This source code was highlighted with Source Code Highlighter.

И xaml к нему
  1. <Window x:Class="WpfApplication1.MainWindow"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication1="clr-namespace:WpfApplication1"
  4.     Title="MainWindow" Height="350" Width="525">
  5.   
  6.   <Window.Resources>
  7.  
  8.     <DataTemplate DataType="{x:Type WpfApplication1:Region}">
  9.       <WrapPanel>
  10.         <TextBlock Text="{Binding Path=Name,StringFormat='{}{0}, '}"/>
  11.         <TextBlock Text="{Binding Path=Index}"/>
  12.       </WrapPanel>
  13.     </DataTemplate>
  14.     
  15.     
  16.   </Window.Resources>
  17.   <Grid>
  18.     <Grid.ColumnDefinitions>
  19.       <ColumnDefinition Width="*"/>
  20.       <ColumnDefinition Width="auto"/>
  21.       <ColumnDefinition Width="*"/>
  22.     </Grid.ColumnDefinitions>
  23.  
  24.     <TreeView Grid.Column="0" ItemsSource="{Binding Items}" BorderThickness="0">
  25.       <TreeView.ItemTemplate>
  26.         <HierarchicalDataTemplate ItemsSource="{Binding SubRegions}">
  27.           <CheckBox IsChecked="{Binding IsSelected,Mode=TwoWay}" Content="{Binding Value}" />          
  28.           
  29.         </HierarchicalDataTemplate>
  30.       </TreeView.ItemTemplate>
  31.     </TreeView>
  32.     <Button Content="Show selected" VerticalAlignment="Center" Grid.Column="1" Click="ButtonClick"/>
  33.     <ListBox Grid.Column="2" ItemsSource="{Binding SelectedItems}" BorderThickness="0" />
  34.   </Grid>
  35. </Window>
* This source code was highlighted with Source Code Highlighter.


Скриншот приложения


image

Ссылки


  1. Исходники проекта
  2. Скомпилированные бинарники
  3. DynamicObject
  4. MVVM
  5. Fluent interface

Спасибо за внимание.
Tags:
Hubs:
+11
Comments 21
Comments Comments 21

Articles