Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Posts Tagged ‘excel

Организация зависимостей в UI после генерации модели

6 комментариев

Как все уже наверное знают, у меня есть система, которая генерит полноценные приложения из Excel таблиц. Пока что эти приложения – только код, но хочется еще и UI впридачу. Причем хочется чтобы поведение UI соответствовало поведению Excel, т.е. чтобы был мгновенный пересчет всего и вся.

Чтобы все это работало правильно, нужно чтобы при изменении чего либо, все другие элементы тоже обновлялись. После обсуждения задачи с @controlflow и другими людьми, стало понятно что есть несколько вариантов, в частности:

  • Переписывать всю модель с использованием INotifyPropertyChanged, т.е. генерить классы и переменные которые все делают соответствующие вызовы (а зависимости резодвить чем-то вроде Obtics). Это конечно вариант, но серьезно нарушает принципы целостности – ведь затаскивание поведения представления в модель – в корне плохо.

  • Делать обертки (вьюхи) поверх существующих POCO классов и привязываться уже к ним. При этом инфа о зависимостях прописывается статически.

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

Я решил выбрать второй вариант как меньшее из всех 3-х зол.

Вью поверх модели

Итак, представим что есть

public class Person
{
  public int Age;
  public bool CanVote()
  {
    return Age >= 16;
  }
}

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

Итак, класс есть, трогать его нельзя. Пишем вьюшку. Точнее, поскольку плодить вызов нотификаций не хочется, тупо делаем абстрактный базовый класс. (Который можно потом менять, например на NotificationObject из Prism.)

public abstract class View : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  [NotifyPropertyChangedInvocator]
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
  }
}

Первое, наивное приближение вьюшки у меня выглядит вот так:

public class PersonView : View
{
  private Person person;
  public PersonView(Person person)
  {
    this.person = person;
  }
  public int Age
  {
    get
    {
      return person.Age;
    }
    set
    {
      if (value == Age) return;
      person.Age = value;
      OnPropertyChanged();
    }
  }
  public bool CanVote()
  {
    return person.CanVote();
  }
}

И в этой вьюшке два недостатка:

  • Непонятно как биндить функцию на UI.

  • При изменении Age, CanVote() должен пересчитываться.

Прикручиваем к формочке

Итак, делаем простую форму для всего этого:

Прописываем простой биндинг:

personView = new PersonView(new Person());
var ageBinding = new Binding("Text", personView, "Age");
tbAge.DataBindings.Add(ageBinding);

Интересно что в WinForms, конверсия из string в int происходит автоматически, в отличии от WPF где нужны конвертеры. Так или иначе, биндинг работает, только вот с CanVote все непонятно.

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

public bool CanVote
{
  get
  {
    return person.CanVote();
  }
}

В этом случае, можно привязываться:

var canVoteBinding = new Binding("Text", personView, "CanVote");
lblCanVote.DataBindings.Add(canVoteBinding);

Нерешенным остается только вопрос о том как сделать NotifyPropertyChanged. Вот так нельзя:

public int Age
{
  get
  {
    return person.Age;
  }
  set
  {
    if (value == Age) return;
    person.Age = value;
    OnPropertyChanged();
    OnPropertyChanged("CanVote"); // FAIL
  }
}

Изменение выше работает, но не масштабируется на уровне 20-страничной экселевой простыни с огромным количеством зависимостей. Ведь зависимости могут быть между объектами, и соответственно между вьюшками.

Таблица зависимостей

Давайте в наш базовый View вклиним таблицу зависимостей (а-а, строковые литералы!) обо всех зависимостях внутри одной вьюшки:

protected Dictionary<string,List<string>> localDependencies = new Dictionary<string, List<string>>();
protected void AddLocalDependency(string from, string to)
{
  if (localDependencies.ContainsKey(from))
    localDependencies[from].Add(to);
  else
  {
    localDependencies.Add(from, new List<string>{to});
  }
}

Теперь, перепишем OnPropertyChanged так, чтобы нотификации уходили не только на «основное» свойство, но и на все что от него зависят:

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
  PropertyChangedEventHandler handler = PropertyChanged;
  if (handler != null)
  {
    handler(this, new PropertyChangedEventArgs(propertyName));
    if (localDependencies.ContainsKey(propertyName))
      foreach (var to in localDependencies[propertyName])
        handler(this, new PropertyChangedEventArgs(to));
  }
}

Теперь, в конструкторе вьюхи можно автогенерировать соответствующий enlisting для пары Age-CanVote, и все должно работать. И оно работает.

Многовьюшные зависимости

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

public class VotingSystem
{
  public int VotingAge;
}
public class VotingSystemView : View
{
  private VotingSystem votingSystem;
  public VotingSystemView(VotingSystem votingSystem)
  {
    this.votingSystem = votingSystem;
  }
  public int VotingAge
  {
    get { return votingSystem.VotingAge; }
    set
    {
      if (value == votingSystem.VotingAge) return;
      votingSystem.VotingAge = value;
      OnPropertyChanged();
    }
  }
}

Теперь Person (именно модель, а не вью) переписывается с использованием VotingSystem:

public class Person
{
  private VotingSystem votingSystem;
  public Person(VotingSystem votingSystem)
  {
    this.votingSystem = votingSystem; // save for later
  }
  public int Age;
  public bool CanVote()
  {
    return Age >= votingSystem.VotingAge; // refer to value
  }
}

Наша цель простая – мы хотим чтобы CanVote() пересчитывался за счет изменений в VotingSystem.VotingAge. Точнее, мы хоти чтобы VotingSystemView мог сказать PersonView «обнови-ка ты свой Age».

Прощайте локальные зависимости

Думаю уже понятно, что точно так же как Person берет VotingSystem в качестве параметра в конструкторе (что полезно для dependency injection), мы делаем то же самое в конструкторе вью, т.е.:

public PersonView(Person person, VotingSystemView votingSystemView)
{
  this.person = person;
}

Кэшировать эту штуку необязательно. Сейчас самое важное – переписать базовый класс так, чтобы нотификации отсылались правильной вьюшке. Тут полезно создать как-то упростить концепцию вью-и-поле:

using VPN = System.Tuple<WindowsFormsApplication1.View.View,string>;

А потом мы меняем наш Dictionary, ну и соответственно механизм вызова тоже меняется:

if (handler != null)
{
  handler(this, new PropertyChangedEventArgs(propertyName));
  var self = new VPN(this, propertyName); // need a pair here
  if (dependencies.ContainsKey(self))
    foreach (var to in dependencies[self])
      to.Item1.OnPropertyChanged(to.Item2); // notify there
}

Ну и собственно enlisting в этот механизм выглядит вот так:

var canVoteTuple = Tuple.Create((View) this, "CanVote");
AddDependency(Tuple.Create((View)this, "Age"), canVoteTuple);
AddDependency(Tuple.Create((View)votingSystemView, "VotingAge"), canVoteTuple);

Постмортем

Есть два варианта как смотреть на все что изложено выше. С одной стороны – это все адов адъ, ведь в действительности у типичного класса может быть 15 зависимостей, что означает что в конструктор воткнется именно столько других объектов, и в конструктор вьюшки тоже.

С другой стороны – оно работает, и это самое главное. Я уверен, что большинство людей, которые сгенерят из Excel-евой простыни полноценное декстоп или веб-приложение не захотят потом кардинально переписывать бизнес-логику. Хотя, кто знает… ■

Written by Dmitri

16 июня 2013 at 14:09

Опубликовано в .NET

Tagged with , , ,