📜 ⬆️ ⬇️

Creating a dynamic proxy object using the dynamic type

Like many people who are faced with the task of writing the next UI for my application, I occasionally encounter the need to create my own model for the UI, which to some extent repeats the domain model, but at the same time expands and / or changes it. And that's what came of it.

Formulation of the problem


For example, we have a Region class of such a structure.
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 .
  1. 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 .
  2. 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 .
  3. 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 .
  4. 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 .
  5. 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 .
  6. 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 .
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 .

And there is a task to show the user in the UI tree of regions, built on this model, as well as give the opportunity to select specific regions that are subject to some further processing. If we make our UI according to MVVM architecture, then it is obvious that we will need to create a class of the RegionViewModel type, with a structure similar to the Region class, but with the additional IsSelected property. In addition, it would be convenient if the RegionViewModel's SubRegions property returned an enumerator of type IEnumerable <RegionViewModel> , then all the UI code would be reduced to creating a TreeView bindig on the RegionViewModel list and getting all RegionViewModel properties that have the IsSelected property true . And what's the problem? The problem is that most of the code of the RegionViewModel class will be data forwarding to the Region class, which it wraps around. Of course, such an implementation is possible, somewhat simplifying life:
  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 .

But it also requires us to implement the SubRegions property with our hands. And if the Region does not implement INotifyPropertyChanged , then most likely we will have to manually describe each property, adding a call to the corresponding event to the value setting.
If a task similar to the one described above occurs quite often, then writing a model-view with my hands each time can be tiring. Therefore, I asked myself the question of automating this process and this is what happened.

Possible solutions


If you look closely, it becomes clear that our task is to create a certain proxy generator that creates a proxy for a given class and complements it with some aspects, such as INotifyPropertyChanged , or / and adds new properties, methods, etc. What does the .net stack offer us to create proxies?

We can distinguish the following solutions

I chose the 5th option, because RealProxy will require intervention in the model classes themselves, the proxy from the castl will allow intercepting only virtual members of the class, postsharp costs money, and I just don’t like code generation for religious reasons. Besides creating your own bike is always more interesting.

Implementation


And so, the main idea concludes as follows - we describe a class that inherits from the type DynamicObject and overrides TryInvokeMember , TrySetMember and TryGetMember .
for example
  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. result = GetResult (result);
  5. return true ;
  6. }
* This source code was highlighted with Source Code Highlighter .

Affairs so that he took at the entrance of the desired object, and when you try to call a member of a class, prokidyval call to this object. Add to this the ability to add new methods and property. For the construction of the object we will use the pillar Fluent builder.
')
The proxy class code itself turned out to be quite large (about 280 lines), so those who want to see it can download the source code and all the examples from the link at the bottom of the post. Here I will give examples of use.

Adding properties


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

Auto-create proxies for function and property values.


  1. [TestMethod]
  2. public void TestChilds ()
  3. {
  4. var a = new MyClass {Name = "123" };
  5. Assert.AreEqual ( "123" , a.Name);
  6. Assert.AreEqual ( "_sdfdsfsdfsfd" , a.Foo (). Name);
  7. var x = 0;
  8. dynamic proxy = DynamicProxy.Create (a) .AddProperty < bool > ( "IsSelected" )
  9. .AddProperty ( "X" , _ => x, (_, value ) => x = value )
  10. .AddProperty ( "LastName" , "FFFF" )
  11. .AddMethod ( "Boo" , new Func <DynamicProxy <MyClass>, int , string > ((m, i) => ((MyClass) m) .Name + i.ToString ())));
  12. proxy.Name = "567" ;
  13. proxy.IsSelected = true ;
  14. proxy.X = 42;
  15. var b = proxy.Foo ();
  16. b.IsSelected = true ;
  17. Assert.AreEqual ( "567" , a.Name);
  18. Assert.AreEqual ( "5674" , proxy.Boo (4));
  19. Assert.IsTrue (proxy.IsSelected);
  20. Assert.AreEqual (42, x);
  21. Assert.IsTrue (b.IsSelected);
  22. b.IsSelected = false ;
  23. Assert.IsTrue (proxy.IsSelected);
  24. Assert.IsFalse (b.IsSelected);
  25. proxy.LastName = "890" ;
  26. var d = proxy.Foo ();
  27. Assert.AreEqual ( "FFFF" , d.LastName);
  28. var d2 = proxy.Foo ();
  29. d2.LastName = "RRRRR" ;
  30. Assert.AreEqual ( "567" , Foo (proxy));
  31. Assert.AreEqual (d.LastName, d2.LastName);
  32. // It is possible to rob the cows to do an impassable caste
  33. var c = (MyClass) proxy;
  34. Assert.AreEqual ( "567" , c.Name);
  35. foreach ( var child in proxy.GetChilds ())
  36. {
  37. child.IsSelected = true ;
  38. Assert.IsTrue (child.IsSelected);
  39. }
  40. }
* This source code was highlighted with Source Code Highlighter .

Automatic implementation of INotifyPropertyChanged is also done .
  1. [TestMethod]
  2. public void TestPropertyChange ()
  3. {
  4. var myClass = new MyClass ();
  5. var propertyName = string .Empty;
  6. dynamic proxy = DynamicProxy.Create (myClass);
  7. ((INotifyPropertyChanged) proxy). PropertyChanged + = (s, a) => propertyName = a.PropertyName;
  8. proxy.Name = "aaaa" ;
  9. Assert.AreEqual ( "Name" , propertyName);
  10. }
* This source code was highlighted with Source Code Highlighter .

Practical use


Let's try using this proxy to solve the problem described in the beginning of the article. To simplify this, we will generate a small tree of regions, let the user select the necessary ones and show the list of selected ones on the right. Actually, this is how the window code is used.
  1. public MainWindow ()
  2. {
  3. Initializecomponent ();
  4. DataContext = this ;
  5. Items = new [] {DynamicProxy.Create (CreateRegions (). First ()). AddProperty < bool > ( "IsSelected" )};
  6. }
  7. IEnumerable <Region> GetSelectedItems ( IEnumerable <dynamic> items)
  8. {
  9. Return items.Where (x => x.IsSelected) .Concat (items.SelectMany (x => GetSelectedItems (( IEnumerable <dynamic>) x.SubRegions))). Select (x => (Region) x);
  10. }
  11. private void ButtonClick ( object sender, RoutedEventArgs e)
  12. {
  13. var res = GetSelectedItems (Items) .Take (10) .ToList ();
  14. SelectedItems = res;
  15. }
* This source code was highlighted with Source Code Highlighter .

And xaml to him
  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. < Window.Resources >
  6. < DataTemplate DataType = "{x: Type WpfApplication1: Region}" >
  7. < WrapPanel >
  8. < TextBlock Text = "{Binding Path = Name, StringFormat = '{} {0},'}" />
  9. < TextBlock Text = "{Binding Path = Index}" />
  10. </ WrapPanel >
  11. </ DataTemplate >
  12. </ Window.Resources >
  13. < Grid >
  14. < Grid.ColumnDefinitions >
  15. < ColumnDefinition Width = "*" />
  16. < ColumnDefinition Width = "auto" />
  17. < ColumnDefinition Width = "*" />
  18. </ Grid.ColumnDefinitions >
  19. < TreeView Grid . Column = "0" ItemsSource = "{Binding Items}" BorderThickness = "0" >
  20. < TreeView.ItemTemplate >
  21. < HierarchicalDataTemplate ItemsSource = "{Binding SubRegions}" >
  22. < CheckBox IsChecked = "{Binding IsSelected, Mode = TwoWay}" Content = "{Binding Value}" />
  23. </ HierarchicalDataTemplate >
  24. </ TreeView.ItemTemplate >
  25. </ TreeView >
  26. < Button Content = "Show selected" VerticalAlignment = "Center" Grid . Column = "1" Click = "ButtonClick" />
  27. < ListBox Grid . Column = "2" ItemsSource = "{Binding SelectedItems}" BorderThickness = "0" />
  28. </ Grid >
  29. </ Window >
* This source code was highlighted with Source Code Highlighter .


Application screenshot


image

Links


  1. Project sources
  2. Compiled binaries
  3. DynamicObject
  4. MVVM
  5. Fluent interface

Thanks for attention.

Source: https://habr.com/ru/post/124078/


All Articles