Статьи

Динамическое добавление RaisePropertyChanged в MVVM Light ViewModels с использованием Reflection.Emit


Мне нравится чистое разделение между представлениями и моделями представлений, которое стало возможным благодаря шаблону 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 # легко возможным. Поскольку я страстный технолог, я, конечно, хотел понять, как работает эта «магия» Glimlach.

Возможны два варианта: посткомпиляция путем изменения скомпилированных сборок или динамическое создание прокси-классов во время выполнения. В этой статье я расскажу, как вы можете реализовать это самостоятельно. Но поскольку доступны различные высококачественные инструменты и инфраструктуры, я рекомендую не реализовывать это самостоятельно для производственного кода.

Посткомпиляция может быть сделана инструментом 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, мы получим:

ILDASM_required_MSIL_code

Имея необходимый код 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>();

Источник:  http://pieterderycke.wordpress.com/2011/07/08/dynamically-adding-raisepropertychanged-to-mvvm-light-viewmodels-using-reflection-emit/