Автоматическая реализация INotifyPropertyChanged посредством генерации кода T4?

В настоящее время я работаю над настройкой моего нового проекта и задаюсь вопросом, как я могу добиться того, что у моих классов ViewModel есть поддержка INotifyPropertyChanged, не имея необходимости вручную вводить все свойства.

Я просмотрел рамки AOP, но я думаю, что они просто взорвут мой проект с другой зависимостью.

Итак, я подумал о создании реализаций свойств с помощью T4.

Настройка будет такой: у меня есть класс ViewModel, который объявляет только его фоновые переменные Properties, а затем я использую T4 для создания из него реализации свойств.

Например, это будет моя ViewModel:

public partial class ViewModel
{
    private string p_SomeProperty;
}

Затем T4 перейдет через исходный файл и ищет объявления участников с именем "p_" и генерирует такой файл:

public partial class ViewModel
{
    public string SomeProperty
    {
        get
        {
            return p_SomeProperty;
        }
        set
        {
            p_SomeProperty= value;
            NotifyPropertyChanged("SomeProperty");
        }
    }
}

Этот подход имеет некоторые преимущества, но я не уверен, действительно ли он может работать. Поэтому я хотел опубликовать свою идею здесь, в StackOverflow, в качестве вопроса, чтобы получить некоторые отзывы об этом и, возможно, некоторые советы, как это можно сделать лучше/проще/безопаснее.

Ответ 1

Здесь отличная статья Колина Эберхардта о создании свойств зависимостей от T4 путем проверки пользовательских атрибутов непосредственно из Visual Studio с помощью EnvDTE. Нетрудно будет ее адаптировать для проверки полей и создания кода соответствующим образом, поскольку сообщение содержит простые методы утилиты для просмотра узлов кода.

Обратите внимание, что при использовании T4 от VS вы не должны использовать Reflection на своих собственных сборках, иначе они будут заблокированы, и вам придется перезапустить Visual Studio для восстановления.

Ответ 2

Существует много способов скинуть эту кошку.

Мы играли с PostSharp для ввода шаблона INotifyProperty. Кажется, это работает очень хорошо.

При этом нет причин, по которым T4 не будет работать.

Я согласен с Дэном в том, что вы должны создать реализацию базового класса OnPropertyChanged.

Рассматривали ли вы просто фрагмент кода? Он напишет шаблон для вас. Единственный недостаток заключается в том, что он не будет обновляться автоматически, если вы хотите изменить имя свойства в какой-то более поздний срок.

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>propin</Title>
      <Shortcut>propin</Shortcut>
      <Description>Code snippet for property and backing field with support for INotifyProperty</Description>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>type</ID>
          <ToolTip>Property type</ToolTip>
          <Default>int</Default>
        </Literal>
        <Literal>
          <ID>property</ID>
          <ToolTip>Property name</ToolTip>
          <Default>MyProperty</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp">
        <![CDATA[private $type$ _$property$;

    public $type$ $property$
    {
        get { return _$property$;}
        set 
    {
      if (value != _$property$)
      {
        _$property$ = value;
        OnPropertyChanged("$property$");
      }
    }
    }
    $end$]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Ответ 3

Он должен определенно работать.

Я бы рекомендовал сначала написать базовый класс, который реализует INotifyPropertyChanged, предоставляя ему метод protected void OnPropertyChanged(string propertyName), заставляя его кэшировать его объекты PropertyChangeEventArgs (по одному для каждого уникального имени свойства - нет смысла создавать новый объект каждый время, когда событие поднято), и ваш T4-генерируемый класс будет получен из этой базы.

Чтобы получить членов, которым нужны реализованные свойства, вы можете просто сделать что-то вроде:

BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
FieldInfo[] fieldsNeedingProperties = inputType.GetFields(flags)
    .Where(f => f.Name.StartsWith("p_"))
    .ToArray();

И идите оттуда:

<# foreach (var field in fieldsNeedingProperties) { #>
<# string propertyName = GetPropertyName(field.Name); #>
    public <#= field.FieldType.FullName #> <#= propertyName #> {
        get { return <#= field.Name #>; }
        set {
            <#= field.Name #> = value;
            OnPropertyChanged("<#= propertyName #>");
        }
    }
<# } #>

<#+
    private string GetPropertyName(string fieldName) {
        return fieldName.Substring(2, fieldName.Length - 2);
    }
#>

И так далее.