Мне нравится чистое разделение между представлениями и моделями представлений, которое стало возможным благодаря шаблону MVVM. Но мне не нравится шаблон кода, необходимый для реализации RaisePropertyChanged в простых свойствах.
private static string someProperty; public string SomeProperty { get { return someProperty; } set { someProperty = value; RaisePropertyChanged("SomeProperty"); } }
Я бы предпочел реализацию как это:
[RaisePropertyChanged] public string SomeProperty { get; set; }
Реализация этого возможна с помощью принципа, называемого AOP (Аспектно-ориентированное программирование). Хотя C # является языком ООП (объектно-ориентированного программирования), различные люди и компании сделали AOP в C # легко возможным. Поскольку я страстный технолог, я, конечно, хотел понять, как работает эта «магия» .
Возможны два варианта: посткомпиляция путем изменения скомпилированных сборок или динамическое создание прокси-классов во время выполнения. В этой статье я расскажу, как вы можете реализовать это самостоятельно. Но поскольку доступны различные высококачественные инструменты и инфраструктуры, я рекомендую не реализовывать это самостоятельно для производственного кода.
Посткомпиляция может быть сделана инструментом AOP, таким как PostSharp . Динамическое создание прокси выполняется библиотеками, такими как Castle DynamicProxy, или контейнерами IOC, такими как Microsoft Unity .
Создание динамического прокси
Я решил использовать динамическое создание прокси в моем примере. Я не буду делать свой код общим, но вместо этого я буду жестко кодировать необходимую функциональность. Я предполагаю, что вы сможете обобщить это самостоятельно, если потребуется. Код основан на MVVM Light , но вы можете заменить эту библиотеку на любую другую библиотеку MVVM.
API, который допускает такое динамическое поведение, является Reflection.Emit и позволяет создавать классы во время выполнения с использованием кода MSIL. MSIL — это сборка .Net CLR. Большинство .Net программистов, вероятно, никогда не видели этот язык. При использовании умного трюка требуются только ограниченные знания.
У Microsoft есть дизассемблер MSIL под названием «ILDASM». Вы можете легко использовать этот инструмент, если запустите его из «командной строки Visual Studio 2010». Чтобы узнать требуемый код MSIL для нашего прототипа, мы сначала создаем то, что хотим в c #, компилируем его, а затем смотрим на сгенерированный код MSIL с помощью ILDASM.
Прокси, который мы хотим создать динамически (SampleViewModelExtended), выглядит следующим образом:
// Dynamic proxy created manually public class SampleViewModelExtended : SampleViewModel { public override string SomeProperty { get { return base.SomeProperty; } set { base.SomeProperty = value; RaisePropertyChanged("SomeProperty"); } } } public class SampleViewModel : ViewModelBase { [RaisePropertyChanged] public virtual string SomeProperty { get; set; } }
Обратите внимание: мы должны сделать свойства виртуальными в базовом классе, потому что иначе мы не сможем перезаписать установщики в производном классе.
Если мы скомпилируем это и посмотрим на MSIL, мы получим:
Имея необходимый код MSIL, единственное, что нам теперь нужно сделать, — это динамически генерировать MSIL с помощью Reflection.Emit.
public static class ReflectionEmitViewModelFactory { public static T CreateInstance<T>() where T : ViewModelBase { Type vmType = typeof(T); VerifyViewModelType(vmType); // Create everything required to get a module builder AssemblyName assemblyName = new AssemblyName("SmartViewModelDynamicAssembly"); AppDomain domain = AppDomain.CurrentDomain; AssemblyBuilder assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); //AssemblyBuilderAccess.RunAndSave); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name); string dynamicTypeName = Assembly.CreateQualifiedName(vmType.AssemblyQualifiedName, "Smart" + vmType.Name); TypeBuilder typeBuilder = moduleBuilder.DefineType(dynamicTypeName, TypeAttributes.Public | TypeAttributes.Class, vmType); MethodInfo raisePropertyChangedMethod = typeof(ViewModelBase).GetMethod("RaisePropertyChanged", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string) }, null); foreach (PropertyInfo propertyInfo in FindNotifyPropertyChangCandidates<T>()) UpdateProperty(propertyInfo, typeBuilder, raisePropertyChangedMethod); Type dynamicType = typeBuilder.CreateType(); return (T)Activator.CreateInstance(dynamicType); } private static void VerifyViewModelType(Type vmType) { if (vmType.IsSealed) throw new InvalidOperationException("The specified view model type is not allowed to be sealed."); } private static IEnumerable<PropertyInfo> FindNotifyPropertyChangCandidates<T>() { return from p in typeof(T).GetProperties() where p.GetSetMethod() != null && p.GetSetMethod().IsVirtual && p.GetCustomAttributes(typeof(RaisePropertyChangedAttribute), false).Length > 0 select p; } private static void UpdateProperty(PropertyInfo propertyInfo, TypeBuilder typeBuilder, MethodInfo raisePropertyChangedMethod) { // Update the setter of the class PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyInfo.Name, PropertyAttributes.None, propertyInfo.PropertyType, null); // Create set method MethodBuilder builder = typeBuilder.DefineMethod("set_" + propertyInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, null, new Type[] { propertyInfo.PropertyType }); builder.DefineParameter(1, ParameterAttributes.None, "value"); ILGenerator generator = builder.GetILGenerator(); // Add IL code for set method generator.Emit(OpCodes.Nop); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Call, propertyInfo.GetSetMethod()); // Call property changed for object generator.Emit(OpCodes.Nop); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldstr, propertyInfo.Name); generator.Emit(OpCodes.Callvirt, raisePropertyChangedMethod); generator.Emit(OpCodes.Nop); generator.Emit(OpCodes.Ret); propertyBuilder.SetSetMethod(builder); } }
Как только вы знаете MSIL для генерации, использовать Reflection.Emit очень просто. Используя приведенный выше пример кода, мы можем очень легко создавать динамические ViewsModels:
SampleViewModel viewModel = ReflectionEmitViewModelFactory.CreateInstance<SampleViewModel>();