Unity Container (3) Property Container

Advanced Unity Container usages

Posted by eagleboost on April 11, 2021

1. The Problem

In the previous post we talked about Reflection.Emit being the best option to implicitly implement the INotifyPropertyChanged interface, with several points that can be improved:

  1. Not debug friendly.
  2. Impact performance when properties are many.
  3. Not easy to get previous value of a property.

These 3 problems are essentially one problem that we need some extra property level logic. Problem would be solved if the property’s Backing field is replaced with something like an IProperty object, as we can see blow:

Let’s start with defining IProperty<T> as the replacement of backing field of a property of type T:

1
2
3
4
5
6
7
8
public interface IProperty<T>
{
  string Name { get; }

  T Value { get; set; }

  event EventHandler<ValueChangedArgs<T>> ValueChanged;
}

Then given ViewModel we emit equivalent IL codes to generate a ViewModel_<PropertyContainer>_Impl class. The PropertyStore class would be responsible for creating and caching instances of IProperty<T>.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Original class
public class ViewModel
{
  public virtual string Name { get; set; }
}
  
//Generated class
public class ViewModel_<PropertyContainer>_Impl : ViewModel
{
  private PropertyStore<ViewModel_<PropertyContainer>_Impl> _store;
  private IProperty<string> _name;

  public ViewModel_<PropertyContainer>_Impl()
  {
    _store = new PropertyStore<ViewModel_<PropertyContainer>_Impl>(this);
    _name = _store.Create<String>("Name");
  }
    
  public IPropertyStore PropertyStore => _store;
  
  public override string Name
  { 
    get => _name.Value; 
    set => _name.Value = value; 
  }
}

Now we can easily fix the 3 problems listed above:

  1. Not debug friendly - Set a break point in the setter of the Value property of an IProperty<T> implementation class.
  2. Impact performance when properties are many - directly listen to the ValueChanged event of IProperty<T>
  3. Not easy to get previous value of a property - EventArgs of the ValueChanged already contains previous and current value of the property.

Since a T now becomes a IProperty<T>, so it uses more memory, but it’s not a big concern in our scenario. This approach apparently is not designed for objects that might have thousands or even millions of instances in the memory. It’s more suitable for writing classes like ViewModels that even in a large scale application total number of ViewModels would not be crazy large.

2. Implementations

The whole process of using Reflection.Emit to generate codes are pretty standard and tedious, we only list the high level steps:

  1. Create a new class and make it derive from BaseType and implement these interfaces:
    • IPropertyContainer - used in the extension methods to retrieve the instance of IPropertyStore created implicitly.
    • INotifyPropertyChanged - the must have interface。
    • IPropertyChangeNotifiable - used to raise PropertyChanged event on the generated class when IProperty<T>.Value is changed.
  2. Implement the INotifyPropertyChanged interface
    • Emit the PropertyChanged event and its backing field.
    • Emit the NotifyPropertyChanged method used to raise the PropertyChanged event.
  3. Emit codes for all public virtual properties of the BaseType:
    • Emit private field of IProperty<T> and call PropertyStore<T>.CreateImplicit() to create instance for the field in the constructor.
    • Emit Getter for the property to read value from IProperty<T>.Value.
    • Emit Setter for the property to set value to IProperty<T>.Value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private static Type CreateImplType(Type baseType)
{
  var assemblyName = new AssemblyName {Name = "<PropertyContainer>_Assembly"};
  var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
  var moduleBuilder = assemblyBuilder.DefineDynamicModule("<PropertyContainer>_Module");
  var typeName = GetImplTypeName(baseType);
  
  ////IPropertyContainerMarker = IPropertyContainer, IPropertyChangeNotifiable, INotifyPropertyChanged
  var interfaces = new[] {typeof(IPropertyContainerMarker)};
  var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public, baseType, interfaces);

  ////Emit the PropertyChanged event and return its field so we can use it to implement the NotifyPropertyChanged method
  var eventFieldBuilder = EmitPropertyChangedField(typeBuilder);
  EmitNotifyPropertyChanged(typeBuilder, eventFieldBuilder);

  ////Emit the constructor and return the ILGenerator, it will be used to emit codes to create the IProperty<T>
  ////backing fields in the ctor
  var ctorIl = EmitCtor(typeBuilder, baseType);
  
  ////Emit the PropertyStore<T> field and corresponding property getter
  //// _store = new PropertyStore<BaseType_<PropertyContainer>_Impl>(this);
  var storeFieldBuilder = EmitPropertyStore(typeBuilder, ctorIl);
  
  ////This would be used to Emit codes like below to create backing fields for each property
  //// _name = _store.CreateImplicit<String>(nameof(PropertyArgs));
  var createPropertyMethod = typeof(PropertyStore<>).GetMethod("CreateImplicit", BindingFlags.Instance | BindingFlags.Public);

  ////Prepare the properties need to be overridden and generate property getter and setter
  var targetProperties = GetTargetProperties(baseType);
  foreach (var property in targetProperties)
  {
    ////type of string
    var propertyType = property.PropertyType;
    var propertyName = property.Name;
    var fieldName = ToFieldName(propertyName);
    ////type of IProperty<string>
    var fieldType = typeof(IProperty<>).MakeGenericType(propertyType);
    var fieldBuilder = typeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Private);

    var fieldInfo = new PropertyFieldInfo(propertyName, propertyType, fieldBuilder);
      
    var valueProperty = fieldType.GetProperty("Value");
    var setValueMethod = valueProperty.GetSetMethod();
    var getValueMethod = valueProperty.GetGetMethod();

    ////private IProperty<string> _name;
    EmitPropertyBackingField(ctorIl, storeFieldBuilder, createPropertyMethod, fieldInfo);
    ////get => _name.Value; 
    EmitPropertyGetter(typeBuilder, property, fieldBuilder, getValueMethod);
    ////set => _name.Value = value; 
    EmitPropertySetter(typeBuilder, property, fieldBuilder, setValueMethod);
  }
  
  ctorIl.Emit(OpCodes.Ret);
  
  return typeBuilder.CreateType();
}

Integration with Unity Container is also simple. We create a PropertyContainerStrategy and add it to the TypeMapping stage.

1
2
3
4
5
6
7
public class UnityPropertyContainerExt : UnityContainerExtension
{
  protected override void Initialize()
  {
    Context.Strategies.Add(new PropertyContainerStrategy(Container), UnityBuildStage.TypeMapping);
  }
}

then check if the dynamic creation process is needed for the current type in the PreBuildUp method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public override void PreBuildUp(ref BuilderContext context)
{
  base.PreBuildUp(ref context);

  var type = context.Type;
  if (_container.IsRegistered(type))
  {
    ////The current type is already registered by calling Container.RegisterType(), do nothing.
    return;
  }

  if (type.IsSubclassOf<IPropertyContainerMarker>())
  {
    ////The IPropertyContainerMarker interface is added dynamically to the generated class by PropertyContainerImpl, it's not supposed to be used directly by any class, so presence of the interface means the current type is already generated by PropertyContainerImpl, so do nothing
    return;
  }
  
  ////Use PropertyContainerTypeKey<T> to check if we need to generate the class for the current type
  if (_container.IsRegistered(typeof(PropertyContainerTypeKey<>).MakeGenericType(type)))
  {
    var implType = PropertyContainerImpl.GetImplType(type);
    context.Type = implType;
    _container.RegisterType(type, implType);
  }
}

Please note that the above PreBuild method works only in Unity Container 4.0 and above. The behavior of determine is a type is registered is different in earlier versions of Unity Container. On one hand, the IsRegistered method is just an extension method that is not production ready, it’s extremely slow and not thread safe. On the other hand, a non-interface type is always registered from Unity Container point of view.

3. Usages

Using it is also straightforward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void Test()
{
  var container = new UnityContainer();

  ////Add the extension to the Unity Container
  container.AddNewExtension<UnityPropertyContainerExt>();
  ////Tell Unity Container to generate the class for ViewModel.
  container.MarkPropertyContainer<ViewModel>();
  
  ////Type of vm will be a class named 'ViewModel_<PropertyContainer>_Impl' and derived from ViewModel
  var vm = container.Resolve<ViewModel>();
  ////Only listen to the Name property's changes
  vm.HookChange(o => o.Name, HandleNameChanged);
  ////Listen to the traditional PropertyChanged event
  vm.HookChange(HandlePropertyChanged);
}

private void HandleNameChanged(object sender, ValueChangedArgs<string> e)
{
  Log($"[Name Changed] {e}");
}

private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
  Log($"[Property Changed] {e.PropertyName}");
}

Similar event listeners also work inside the ViewModel class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ViewModel
{
  public virtual string Name { get; set; }
  
  public virtual int Age { get; set; }
  
  [InjectionMethod]
  public void Init()
  {
    this.HookChange(o => o.Name, HandleNameChanged);
    this.HookChange(HandlePropertyChanged);
  }

  private void HandleNameChanged(object sender, ValueChangedArgs<string> e)
  {
    Log($"[Name Changed] {e}");
  }
  
  private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
  {
    Log($"[Property Changed] {e.PropertyName}");
  }
}

Please visit github for complete implementations.

4. Applications

Now we can already generate codes to automatically implement the INotifyPropertyChanged interface for classes, and fixed some problems by introducing IProperty<T>. This approach not only simplifies the process of writing ViewModel classes, it can also change the way some other functions were done before and more applications can be developed based on it:

For example, there’re cases we want to log all property changes. This job usually requires reflection to access property values when the. Now we just need to call IPropertyStore.GetProperties() and listen to ValueChanged event of all properties, then access property value directly.

The second example is input validation. WPF already supports validation at Data Binding level. Validation rules can be configured in XAML, or in M-V-VM world ViewModels can also implement either IDataErrorInfo or INotifyDataErrorInfo interface (for asynchronous scenarios) and do the validation at data level. The concept is simple but like the INotifyPropertyChanged interface, similar problems still exist:

  1. The whole process of implementing IDataErrorInfo is pretty standard but not simple.
  2. The IDataErrorInfo interface has to be implemented by the ViewModel itself (Binding Source), which makes the ViewModel fat and mess, also not easy to reuse.
  3. When IDataErrorInfo is used, data binding is applied to to indexer, which means we’ll need to raise PropertyChanged event with an event args named Item[] and cause refresh on all of the properties. It’s a small problem but not necessary.

Another example made easy is also pretty common in UI applications - dirty tracking. WPF does not have built-in support for dirty tracking. One common approach used by developers is exposing a IsDirty property on the ViewModel and set it to true when the PropertyChanged event is triggered. This approach also has some obvious problems:

  1. Dirty tracking is an optional function but the implementation could easily mess with business logics.
  2. Maintaining the state of IsDirty could be troublesome as anyone can set the property to true.
  3. Lack of real dirty tracking. Assume user change one property from A to B then change it back to A, although the PropertyChanged event is triggered multiple times but IsDirty should be False instead of True

Based on the IProperty idea, next we’ll discuss reusable solutions to fix all of the problems.

1. 问题

前文在解决自动实现INotifyPropertyChanged接口问题之外还提出了几个遗留问题:

  1. 调试不方便
  2. 属性过多时监听事件带来不必要的性能损失
  3. 不易获取属性之前的值

这三个问题归纳起来其实是一个问题,那就是需要提供属性而不是对象层面的处理——如果把一个属性的Backing field升级为一个对象,这几个问题就将迎刃而解。

先定义如下接口来代替一个类型为T的属性的Backing field

1
2
3
4
5
6
7
8
public interface IProperty<T>
{
  string Name { get; }

  T Value { get; set; }

  event EventHandler<ValueChangedArgs<T>> ValueChanged;
}

再生成代码把下面的ViewModel展开为ViewModel_<PropertyContainer>_Impl。其中PropertyStore负责根据属性名创建并缓存IProperty<T>的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//原始类
public class ViewModel
{
  public virtual string Name { get; set; }
}
  
//动态生成的类
public class ViewModel_<PropertyContainer>_Impl : ViewModel
{
  private PropertyStore<ViewModel_<PropertyContainer>_Impl> _store;
  private IProperty<string> _name;

  public ViewModel_<PropertyContainer>_Impl()
  {
    _store = new PropertyStore<ViewModel_<PropertyContainer>_Impl>(this);
    _name = _store.Create<String>("Name");
  }
    
  public IPropertyStore PropertyStore => _store;
  
  public override string Name
  { 
    get => _name.Value; 
    set => _name.Value = value; 
  }
}

解决上面的几个问题现在我们可以这样:

  1. 调试不方便——在IProperty<T>实现类Value属性的Setter里设置断点。
  2. 属性过多时监听事件带来不必要的性能损失——根据属性名获得相应IProperty<T>的实例并直接监听其ValueChanged事件。
  3. 不易获取属性之前的值——ValueChanged事件的EventArgs已经包含属性的之前值和当前值。

当然这样做的代价是内存开销变大,但首先来说没有银弹,不存在可以用来解决一切问题的方法,我们总是需要具体情况具体分析。这种方法适用的对象并不是那种有几十上百个属性同时还有成千上万甚至几十上百万个实例的类。这种方法更适用于编写ViewModel——即便是超大型应用程序也不大可能有成千上万个ViewModel,这种情况下额外的内存开销完全可以接受。

2. 实现

使用Reflection.Emit生成代码的过程比较繁复,仅把主干陈列如下,大体工作分为下面几步:

  1. 创建一个类继承自BaseType并实现几个接口:
    • IPropertyContainer——用于扩展方法从对象获取其隐式生成的IPropertyStore实例。
    • INotifyPropertyChanged——生成的类需要实现该接口,保持兼容性。
    • IPropertyChangeNotifiable——用于当IProperty<T>.Value发生改变时也在类上触发PropertyChanged事件。
  2. 实现INotifyPropertyChanged接口
    • 生成PropertyChanged事件及其私有成员变量。
    • 生成NotifyPropertyChanged方法来触发PropertyChanged事件。
  3. BaseType所有virtual的公开自动属性生成代码:
    • 生成IProperty<T>私有成员变量并在构造函数里生成代码调用PropertyStore<T>.CreateImplicit()方法为该成员创建实例。
    • 为属性生成Getter来从IProperty<T>.Value读取数据。
    • 为属性生成Setter来把数据写入IProperty<T>.Value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private static Type CreateImplType(Type baseType)
{
  var assemblyName = new AssemblyName {Name = "<PropertyContainer>_Assembly"};
  var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
  var moduleBuilder = assemblyBuilder.DefineDynamicModule("<PropertyContainer>_Module");
  var typeName = GetImplTypeName(baseType);
  
  ////IPropertyContainerMarker = IPropertyContainer, IPropertyChangeNotifiable, INotifyPropertyChanged
  var interfaces = new[] {typeof(IPropertyContainerMarker)};
  var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public, baseType, interfaces);

  ////Emit the PropertyChanged event and return its field so we can use it to implement the NotifyPropertyChanged method
  var eventFieldBuilder = EmitPropertyChangedField(typeBuilder);
  EmitNotifyPropertyChanged(typeBuilder, eventFieldBuilder);

  ////Emit the constructor and return the ILGenerator, it will be used to emit codes to create the IProperty<T>
  ////backing fields in the ctor
  var ctorIl = EmitCtor(typeBuilder, baseType);
  
  ////Emit the PropertyStore<T> field and corresponding property getter
  //// _store = new PropertyStore<BaseType_<PropertyContainer>_Impl>(this);
  var storeFieldBuilder = EmitPropertyStore(typeBuilder, ctorIl);
  
  ////This would be used to Emit codes like below to create backing fields for each property
  //// _name = _store.CreateImplicit<String>(nameof(PropertyArgs));
  var createPropertyMethod = typeof(PropertyStore<>).GetMethod("CreateImplicit", BindingFlags.Instance | BindingFlags.Public);

  ////Prepare the properties need to be overridden and generate property getter and setter
  var targetProperties = GetTargetProperties(baseType);
  foreach (var property in targetProperties)
  {
    ////type of string
    var propertyType = property.PropertyType;
    var propertyName = property.Name;
    var fieldName = ToFieldName(propertyName);
    ////type of IProperty<string>
    var fieldType = typeof(IProperty<>).MakeGenericType(propertyType);
    var fieldBuilder = typeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Private);

    var fieldInfo = new PropertyFieldInfo(propertyName, propertyType, fieldBuilder);
      
    var valueProperty = fieldType.GetProperty("Value");
    var setValueMethod = valueProperty.GetSetMethod();
    var getValueMethod = valueProperty.GetGetMethod();

    ////private IProperty<string> _name;
    EmitPropertyBackingField(ctorIl, storeFieldBuilder, createPropertyMethod, fieldInfo);
    ////get => _name.Value; 
    EmitPropertyGetter(typeBuilder, property, fieldBuilder, getValueMethod);
    ////set => _name.Value = value; 
    EmitPropertySetter(typeBuilder, property, fieldBuilder, setValueMethod);
  }
  
  ctorIl.Emit(OpCodes.Ret);
  
  return typeBuilder.CreateType();
}

Unity Container的集成非常简单。首先添加一个TypeMapping阶段的的构造策略PropertyContainerStrategy

1
2
3
4
5
6
7
public class UnityPropertyContainerExt : UnityContainerExtension
{
  protected override void Initialize()
  {
    Context.Strategies.Add(new PropertyContainerStrategy(Container), UnityBuildStage.TypeMapping);
  }
}

然后在PreBuildUp方法中检查是否需要为当前类型创建动态类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public override void PreBuildUp(ref BuilderContext context)
{
  base.PreBuildUp(ref context);

  var type = context.Type;
  if (_container.IsRegistered(type))
  {
    ////已经为当前类型显示调用了Container.RegisterType(),直接返回
    return;
  }

  if (type.IsSubclassOf<IPropertyContainerMarker>())
  {
    ////IPropertyContainerMarker接口是PropertyContainerImpl为生成的代码添加的接口。该接口存在表示当前类型已经是PropertyContainerImpl生成的类型,无需继续
    return;
  }
  
  ////用PropertyContainerTypeKey<T>来确定是否需要为当前类生成代码
  if (_container.IsRegistered(typeof(PropertyContainerTypeKey<>).MakeGenericType(type)))
  {
    var implType = PropertyContainerImpl.GetImplType(type);
    context.Type = implType;
    _container.RegisterType(type, implType);
  }
}

此处PreBuild所演示的代码仅限于Unity Container 4.0及以后版本。检查某个类型是否已经注册的行为在早期版本并不相同。早期版本中一方面IsRegistered方法仅仅作为一个供测试用的扩展方法存在,不仅低效而且并不线程安全,需要通过编写自定义代码重新实现。另一方面调用一个非接口的类型对于Unity Container来说始终是已经注册的状态。

3. 使用

使用非常简单,示例及注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void Test()
{
  var container = new UnityContainer();

  ////把扩展添加到Unity Container
  container.AddNewExtension<UnityPropertyContainerExt>();
  ////告诉Unity Container需要为ViewModel类生成代码。
  container.MarkPropertyContainer<ViewModel>();
  
  ////vm的类型会是一个继承自ViewModel,类型名为"ViewModel_<PropertyContainer>_Impl"的类
  var vm = container.Resolve<ViewModel>();
  ////仅监听Name属性的变化
  vm.HookChange(o => o.Name, HandleNameChanged);
  ////监听传统PropertyChanged事件
  vm.HookChange(HandlePropertyChanged);
}

private void HandleNameChanged(object sender, ValueChangedArgs<string> e)
{
  Log($"[Name Changed] {e}");
}

private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
  Log($"[Property Changed] {e.PropertyName}");
}

同样的事件监听代码在ViewModel内部也可以工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ViewModel
{
  public virtual string Name { get; set; }
  
  public virtual int Age { get; set; }
  
  [InjectionMethod]
  public void Init()
  {
    this.HookChange(o => o.Name, HandleNameChanged);
    this.HookChange(HandlePropertyChanged);
  }

  private void HandleNameChanged(object sender, ValueChangedArgs<string> e)
  {
    Log($"[Name Changed] {e}");
  }
  
  private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
  {
    Log($"[Property Changed] {e.PropertyName}");
  }
}

完整实现请移步github

4. 结语

自此我们基本解决了为一个类自动生成INotifyPropertyChanged接口的问题,并且引入IProperty<T>的概念消除了调试及性能问题。更多的应用可以在此基础上展开,比如:

  1. 把所有属性的值改变过程写入日志。这在过去是一项常常会涉及反射的繁琐工作,现在只需要从生成的对象调用IPropertyStore.GetProperties()方法,监听每个属性的ValueChanged事件即可,这项工作很容易创建为一个可重用的组件。
  2. 属性值校验。WPF在数据绑定层面提供了属性值校验的机制,ViewModel只需实现IDataErrorInfo或者INotifyDataErrorInfo接口(用于异步场合)。看似简单,但与INotifyPropertyChanged接口类似的问题同样存在:首先IDataErrorInfo接口的实现大同小异而繁琐。其次该接口要求ViewModel本身而不是其它对象来实现(通过绑定来隐式发现),极易导致ViewModel代码混乱,重用也不容易。第三,虽然是小问题,但也存在一个属性变化触发无关属性被更新的问题。接下来我们将会讨论一种方法解决所有问题。
  3. 脏数据跟踪。这也是有UI的应用程序常见的需求,WPF并未提供对脏数据跟踪的内建支持。一个常见的简单做法是让ViewModel暴露一个IsDirty属性,当其余任何属性发生变化时把IsDirty设置为True。这种方法有几个缺点,一是ViewModel需要包含又一个可选的额外机制,容易导致代码混乱。二是IsDirty状态的维护会是个问题,因为谁可以把它设置为True。三是缺乏真正的脏数据跟踪,比如用户把一个属性的值从A改成B再改回A,虽然PropertyChanged事件触发了几次,但IsDirty应该是False而不是True

基于IProperty的概念,我们将能够轻松编写可重用并且可插拔的组件来解决上述几种应用场景的问题。