Реализация обновления Model из ViewModel (MVVM pattern, update Model from ViewModel)

Рейтинг: 3Ответов: 1Опубликовано: 09.04.2015

В процессе изучения MVVM натолкнулся на проблему обновления данных в Model, полученных из ViewModel , имеется некое приложение, в котором:

Model:

public class Journal:IJournal<Common,Excavation>
{
    public Common CommonInfo { get; set; }
    public ObservableCollection<Excavation> Excavations { get; set; }
}

public class Common:ICommon
{
    public String Object { get; set; }
    public UInt32 NumJournal { get; set; }
} 

public class Excavation:IExcavation
{
    public String NumExcavation { get; set; }
    public DateTime DateCreateExcavation { get; set; }
}

ViewModel:

class JournalViewModel : PropertyChangedNotification,IJournal<CommonViewModel,ExcavationViewModel>
{
    public CommonViewModel CommonInfo
    {
        get { return GetValue(() => CommonInfo); }
        set { SetValue(() => CommonInfo, value); }
    }

    public ObservableCollection<ExcavationViewModel> Excavations
    {
        get { return GetValue(() => Excavations); }
        set { SetValue(() => Excavations, value); }
    }

    public JournalViewModel(Journal journal)
    {
        CommonInfo = new CommonViewModel(journal.CommonInfo);
        Excavations = new ObservableCollection<ExcavationViewModel>();
        foreach (var excavation in journal.Excavations)
        {
            Excavations.Add(new ExcavationViewModel(excavation));
        }
    }
}

class CommonViewModel : PropertyChangedNotification,ICommon
{
    public String Object 
    { 
      get { return GetValue(() => Object ); }
      set { SetValue(() => Object , value); }
    }

    public UInt32 NumJournal 
    { 
      get { return GetValue(() => NumJournal ); }
      set { SetValue(() => NumJournal , value); }
    }

    public CommonViewModel(Common common)
    {
        Object = common.Object;
        NumJournal = common.NumJournal ;
    }
}

class ExcavationViewModel : PropertyChangedNotification, IExcavation
{
    public String NumExcavation 
    { 
      get { return GetValue(() => NumExcavation ); }
      set { SetValue(() => NumExcavation , value); }
    }

    public DateTime DateCreateExcavation 
    { 
      get { return GetValue(() => DateCreateExcavation ); }
      set { SetValue(() => DateCreateExcavation , value); }
    }

    public ExcavationViewModel(Excavation excavation)
    {
        NumExcavation = common.NumExcavation ;
        DateCreateExcavation = common.DateCreateExcavation ;
    }
}

Думаю будет совершенно не критично, если обойтись без View. В приведенном примере кода четко видно, что объект класса Journal передается в конструктор JournalViewModel и после чего, данные из этого объекта заносятся в свойства VM. Однако заносятся только данные, т.е. VM с M по сути не связаны. Возникает вопрос, а что если модель предназначена для сериализации и десериализации в определенный формат файла и работает именно с ним, ведь нужно обновлять данные модели...

Начитавшись статей, нашел 3 различных способа решить проблему обновления модели из VM:

1) Создать метод в VM который будет обновлять всю модель сразу (действия обратные действиям в конструкторе в полученный объект Journal записывать данные из свойств VM)

2) Создать некие Event отслеживающие изменения VM и записывающие данные в M

3) Создать свойство хранящее начальный объект Journal и в конструкторе записывать в него принимаемого значения после чего в getter и setter свойств использовать return Journals.Common (например) Однако, есть некоторая проблема с типами не могу понять, где ссылка работает, а где перестает действовать. Потому что я передаю объект (ссылку) Journal в конструктор VM, он так же содержит внутри себя Common который тоже является объектом (ссылкой), но связывания не происходит передаются только данные.

В данном примере

class BookViewModel : ViewModelBase
{
    public Book Book;

    public BookViewModel(Book book)
    {
        this.Book = book;
    }

    public string Title
    {
        get { return Book.Title; }
        set 
        {
            Book.Title = value;
            OnPropertyChanged("Title");
        }
    }
}

Связь VM с M реализована как раз через ссылку, не пойму правильно ли так их связывать в условиях паттерна, ведь по сути мы просто протягиваем модель и в нее через привязку к представлению записываем данные... В данной статье видел картинку в ней указано что VM общаясь с M использует 3 вида связи:

  • События изменения модели
  • Обновление модели
  • Получение данных

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

Ответы

▲ 2Принят

Как оказалось, действительно существует несколько способов взаимодействия с моделью. А использовать прямую связь с моделью, можно, как в этом примере:

class BookViewModel : ViewModelBase
{
    public Book Book;

    public BookViewModel(Book book)
    {
        this.Book = book;
    }

    public string Title
    {
        get { return Book.Title; }
        set 
        {
            Book.Title = value;
            OnPropertyChanged("Title");
        }
    }
}

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

Если же Модель требуется обновляться во время изменения информации в VM сразу же (что может требоваться в любой системе) то нужно использовать Event-ы отслеживающие изменения VM и записывающие данные в M. К сожалению я не нашел примера =(

Однако моя задача сводилась к тому, что изменение в VM можно было бы вносить в M в любое время, и не важно, делать это сразу (Event) или во время выполнения команды (например сохранения).
Для этого код классов VM был дополнен методами GetModel:

class JournalViewModel : PropertyChangedNotification,IJournal<CommonViewModel,ExcavationViewModel>
{
    public CommonViewModel CommonInfo
    {
        get { return GetValue(() => CommonInfo); }
        set { SetValue(() => CommonInfo, value); }
    }

    public ObservableCollection<ExcavationViewModel> Excavations
    {
        get { return GetValue(() => Excavations); }
        set { SetValue(() => Excavations, value); }
    }

    public JournalViewModel(Journal journal)
    {
        CommonInfo = new CommonViewModel(journal.CommonInfo);
        Excavations = new ObservableCollection<ExcavationViewModel>();
        foreach (var excavation in journal.Excavations)
        {
            Excavations.Add(new ExcavationViewModel(excavation));
        }
    }

    public Journal GetModel()
    {
        ObservableCollection<Excavation> excavations = null;            
        if(Excavations!=null)
        {
            excavations = new ObservableCollection<Excavation>();
            foreach(var excavation in Excavations)
                excavations.Add(excavation.GetModel())
        }
        return new Common 
        {
            Excavations= excavations ,
            CommonInfo = CommonInfo.GetModel() 

        };
    }        

}

class CommonViewModel : PropertyChangedNotification,ICommon
{
    public String Object 
    { 
      get { return GetValue(() => Object ); }
      set { SetValue(() => Object , value); }
    }

    public UInt32 NumJournal 
    { 
      get { return GetValue(() => NumJournal ); }
      set { SetValue(() => NumJournal , value); }
    }

    public CommonViewModel(Common common)
    {
        Object = common.Object;
        NumJournal = common.NumJournal ;
    }

    public Common GetModel()
    {
        return new Common 
        {
            Object = Object ,
            NumJournal = NumJournal

        };
    }
}

class ExcavationViewModel : PropertyChangedNotification, IExcavation
{
    public String NumExcavation 
    { 
      get { return GetValue(() => NumExcavation ); }
      set { SetValue(() => NumExcavation , value); }
    }

    public DateTime DateCreateExcavation 
    { 
      get { return GetValue(() => DateCreateExcavation ); }
      set { SetValue(() => DateCreateExcavation , value); }
    }

    public ExcavationViewModel(Excavation excavation)
    {
        NumExcavation = common.NumExcavation ;
        DateCreateExcavation = common.DateCreateExcavation ;
    }

    public Excavation GetModel()
    {
        return new Excavation
        {
            NumExcavation = NumExcavation ,
            DateCreateExcavation  = DateCreateExcavation 
        };

    }

}

Если у людей читающих этот ответ будет возможность ответить на вопрос: "Как организовать обновление Model во время изменения VM", буду очень благодарен. Иметь представление, что делается это через Event - хорошо, но видеть пример - ещё лучше. Так же буду благодарен за советы и комментарии касательно использованного способа (GetModel).

EDIT 25.06.2015
После точных замечаний @VladD, я решил внести некоторые изменения, возможно они будут правильнее нынешней реализации. И ведь действительно везде есть свои плюсы и минусы. В моей реализации, пользователь может вести работу сразу с несколькими журналами, предположим с 10. Сейчас я храню коллекцию не из 10 Journal (модели), а из 10 JournalViewModel (далее буду называть JourVM или просто VM), естественно JourVM содержит в себе множество свойств, полей, методов и команд, в таком случае, я не храню данные в виде объекта Journal в памяти (именно в объект модели Journal сериализуются данные из JSON, после чего грузятся в JourVM через конструкторы), а храню все JourVM в памяти и при необходимости вызываю метод GetModel() выгружаю все данные в объект модели только чтобы сериализовать модель в JSON.

В этом случае я вроде бы выигрываю немного памяти с тем, что не храню Journal (убирается после того как отдал данные в VM), однако перегружаю всю систему храня кучу VM =(

Видимо правильней было бы хранить коллекцию из Journal (List) и при переключении вкладок, используя interaction обращаться к модели, выгружая данные из модели в VM, при этом провести объекты модели через все VM для мгновенных изменений в модели, если разобрать на примере кода описанном выше, получится:

class JournalViewModel : PropertyChangedNotification, IJournal<CommonViewModel, ExcavationViewModel>
    {
        public CommonViewModel CommonInfo
        {
            get { return GetValue(() => CommonInfo); }
            set
            { SetValue(() => CommonInfo, value);}
        }

        public ObservableCollection<ExcavationViewModel> Excavations
        {
            get { return GetValue(() => Excavations); }
            set { SetValue(() => Excavations, value); }
        }

        private Journal _JournalM;

        public JournalViewModel(Journal journal)
        {
            _JournalM = journal;

            CommonInfo = new CommonViewModel(_JournalM.CommonInfo);
            Excavations = new ObservableCollection<ExcavationViewModel>();
            foreach (var excavation in _JournalM.Excavations)
            {
                Excavations.Add(new ExcavationViewModel(excavation));
            }
        }

    }

    class CommonViewModel : PropertyChangedNotification, ICommon
    {
        public String Object
        {
            get { return GetValue(() => Object); }
            set
            {
                SetValue(() => Object, value);
                _CommonM.Object = Object;
            }
        }

        public UInt32 NumJournal
        {
            get { return GetValue(() => NumJournal); }
            set
            {
                SetValue(() => NumJournal, value);
                _CommonM.NumJournal = NumJournal;
            }
        }

        private CommonInfo _CommonM;

        public CommonViewModel(Common common)
        {
            _CommonM = common;
            Object = _CommonM.Object;
            NumJournal = _CommonM.NumJournal;
        }

    }

    class ExcavationViewModel : PropertyChangedNotification, IExcavation
    {
        public String NumExcavation
        {
            get { return GetValue(() => NumExcavation); }
            set
            {
                SetValue(() => NumExcavation, value);
                _ExcavationM.NumExcavation = NumExcavation;
            }
        }

        public DateTime DateCreateExcavation
        {
            get { return GetValue(() => DateCreateExcavation); }
            set
            {
                SetValue(() => DateCreateExcavation, value);
                _ExcavationM.DateCreateExcavation = DateCreateExcavation;
            }
        }

        private Excavation _ExcavationM;

        public ExcavationViewModel(Excavation excavation)
        {
            _ExcavationM = excavation;
            NumExcavation = _ExcavationM.NumExcavation;
            DateCreateExcavation = _ExcavationM.DateCreateExcavation;
        }
    }

По идее в конструктор VM поступает ссылка на M, мы ссылаемся на нее в VM и у нас не тратится драгоценная память, при изменении информации она сразу же меняется в нужной области в M и не требуется переключаться между VM. Сейчас мое приложение реализовано через TabControl-ы.
1) TabControl имеет вкладки Журналы, Настройки Справка и в качестве DataContex имеет MainVM который в свою очередь хранит VM с коллекцией журналов, отдельную VM с настройками и VM со справкой.
2) TabControl в качестве вкладок использует журналы из коллекции (DataContex на коллекцию из VM) хранящий ещё один TabControl с ссылками вкладок на определенные части VM. что вызывает некоторые тормоза в графике =(
Если я все ещё не правильно интерпретирую данную мне информацию к размышлению и исправлению структуры и логики приложения это очень печально.