пятница, 21 февраля 2014 г.

Entity Framework 6.0.x и INotifyPropertyChanged

При разработке очередного приложения с использованием WPF и Entity Framework 6.0.2 наткнулся на проблему привязки данных. При связывании данных представления с моделью представления, в которой находится заполненная модель таблицы, не происходит обновление представления при изменении данных в привязанной модели таблицы.

Причина стала понятна, когда я открыл код связываемой с представлением модели таблицы. Там не было реализации интерфейса INotifyPropertyChanged.

На моем последнем проекте из мира Silverlight / WPF, где я принимал участие, в качестве ORM использовался LINQ to SQL. Там, при генерации модели, генератором реализовывался интерфейс INotifyPropertyChanged. И при связывании данных, если к представлению напрямую привязывались поля из модели таблицы, проблем не возникало.

На еще одном другом моем проекте используется до сих пор Entity Framework 5.x. Дак вот там этот интерфейс тоже реализуется. Но там к модели не добавляется файл со скриптом шаблона по которому происходит генерация моделей таблиц, а в 6.0.x версии добавляется.

Почему Microsoft сделала такую подлянку, точно известно только им. Ну а мне надо искать пути решения возникшей ситуации.

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

После пары часов экспериментов, я убедился, что идея хорошая, т.к. скрипт выглядит довольно не сложно, хоть он и не маленьких размеров, порядка 800 - 900 строк. Он содержит в себе код на C# (зависит от языка проекта, в который добавляется этот скрипт) с помесью какого-то специального диалекта. После сохранения внесенных в код скрипта изменений, модели таблиц автоматически перестраиваются. И мы тут же можем наблюдать результат своих изменений. Что довольно удобно.

Нам надо модифицировать в скрипте генерацию следующих моментов:
  1. Модифицировать заголовок генерируемого класса (добавить наследование от интерфейса INotifyPropertyChanged).
  2. Добавить реализацию интерфейса INotifyPropertyChanged.
  3. Модифицировать реализацию свойств. В скрипте генерируются 3 типа свойств: 
    • Обычные скалярные (поля в таблицах).
    • Навигационные, реализующие переходы по связям таблиц.
    • Свойства DbSet (с ними пока не разобрался, но вроде как их трогать тоже не надо).
Раскрываем в дереве проекта файл *.edmx. И видим в нем еще 2 файла с расширением tt. Один *.Content.tt (он отвечает за генерацию контекста данных) и второй просто *.tt (он отвечает за генерацию модели данных). Нас интересует именно второй вариант (*.tt). Его то и надо модифицировать.



Делаем резервную копию файла. И начинаем вносить изменения в код скрипта. По пунктам.

1. Заголовок генерируемого класса.
Находим в коде скрипта метод:
    public string EntityClassOpening(EntityType entity)
И модифицируем его следующим образом:
    
    public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            @"{0} {1}partial class {2} : System.ComponentModel.INotifyPropertyChanged{3}",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            _code.StringBefore(" , ", _typeMapper.GetTypeName(entity.BaseType)));
    }
Как видно, я всего лишь дописал к генерируемому типу класса наследование от нужного мне интерфейса INotifyPropertyChanged и перенес добавление дополнительных наследуемых типов в конец.
Сохраняем и видим, что в наших моделях таблиц в классах появилось наследование от интерфейса INotifyPropertyChanged. То что нам нужно.

2. Реализация интерфейса INotifyPropertyChanged.
Находим в коде скрипта следующие две строчки:
<#=codeStringGenerator.UsingDirectives(inHeader: false)#>
<#=codeStringGenerator.EntityClassOpening(entity)#>
И вносим после этих двух строчек и следующей после фигурной скобки следующие изменения:
<#=codeStringGenerator.UsingDirectives(inHeader: false)#>
<#=codeStringGenerator.EntityClassOpening(entity)#>
{
 
 #region Implement INotifyPropertyChanged
 
 public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
 
 protected virtual void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
  {
   PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
  }
 }
 
 #endregion
 
<#
    var propertiesWithDefaultValues = typeMapper.GetPropertiesWithDefaultValues(entity);
    var collectionNavigationProperties = typeMapper.GetCollectionNavigationProperties(entity);
    var complexProperties = typeMapper.GetComplexProperties(entity);
 
    if (propertiesWithDefaultValues.Any() || collectionNavigationProperties.Any() || complexProperties.Any())
    {
#>
Сохраняем и любуемся перегенерированными классами моделей таблиц. У нас появилась реализация интерфейса INotifyPropertyChanged.

3. Генерируемые свойства.
  • Скалярные свойства.
Находим в коде скрипта метод:
    public string Property(EdmProperty edmProperty)
И модифицируем его следующим образом:
    public string Property(EdmProperty edmProperty)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            @"private {1} _{2};
 {0} {1} {2} 
 {{ 
  {3}get
  {{
   return _{2};
  }} 
  {4}set
  {{
   if(_{2} != value)
   {{
    _{2} = value;
    OnPropertyChanged(""{2}"");
   }}
  }}
 }}
 ",
            Accessibility.ForProperty(edmProperty),
            _typeMapper.GetTypeName(edmProperty.TypeUsage),
            _code.Escape(edmProperty),
            _code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
    }
Сохраняем и видим, что в наших моделях таблиц в скалярных свойствах появилось использование реализации интерфейса INotifyPropertyChanged.
До изменений шаблон свойства выглядел примерно следующим образом:
@"{0} {1} {2} {{ get; set; }}"
Как видно, я всего лишь его чутка дописал до нужного мне вида.


  • Навигационные свойства

Находим в коде скрипта метод:
    public string NavigationProperty(NavigationProperty navigationProperty)
И модифицируем его как указано ниже:
    public string NavigationProperty(NavigationProperty navigationProperty)
    {
        var endType = _typeMapper.GetTypeName(navigationProperty.ToEndMember.GetEntityType());
        return string.Format(
            CultureInfo.InvariantCulture,
            @"private {1} _{2};
 {0} {1} {2} 
 {{ 
  {3}get
  {{
   return _{2};
  }} 
  {4}set
  {{
   if(_{2} != value)
   {{
    _{2} = value;
    OnPropertyChanged(""{2}"");
   }}
  }}
 }}
 ",
            AccessibilityAndVirtual(Accessibility.ForProperty(navigationProperty)),
            navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
            _code.Escape(navigationProperty),
            _code.SpaceAfter(Accessibility.ForGetter(navigationProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(navigationProperty)));
    }
Сохраняем и наблюдаем, как в наших моделях таблиц навигационные  свойства перестроились нужным нам образом, чтобы уведомлять своих подписчиках об изменениях в связях между таблицами.

Собственно вот и все. Таким образом можно заставить Entity Framework 6.0.x генерировать модель таблиц любым, необходимым Вам образом.

5 комментариев:

  1. Очень нужный материал, спасибо.

    ОтветитьУдалить
  2. Святослав, спасибо!
    Хорошая работа. Но в идеале, скрипт нужно еще допилить для корректной обработки классов, создаваемых для хранимых процедур. Для них не модифицируется заголовок создаваемого класса (наследование от интерфейса INotifyPropertyChanged)

    ОтветитьУдалить
    Ответы
    1. Пожалуйста! Рад, что помог =)
      Про хранимые процедуры - верно подмечено. Но у меня их не было в проекте. Поэтому и не задумался об этом.

      Удалить
  3. Для решения этого, ищем:
    <#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#>
    {
    <#

    var complexProperties = typeMapper.GetComplexProperties(complex);
    var propertiesWithDefaultValues = typeMapper.GetPropertiesWithDefaultValues(complex);
    меняем на
    <#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#><#=" : System.ComponentModel.INotifyPropertyChanged"#>
    {
    #region Implement INotifyPropertyChanged

    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
    if (PropertyChanged != null)
    {
    PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
    }
    }

    #endregion
    <#

    var complexProperties = typeMapper.GetComplexProperties(complex);
    var propertiesWithDefaultValues = typeMapper.GetPropertiesWithDefaultValues(complex);

    ОтветитьУдалить