WCF RIA Services. Обновление данных. Часть 3

в 11:00, , рубрики: Без рубрики

В предыдущем уроке мы более подробно ознакомились с возможностями получения данных в WCF RIA Services. Сегодня поговорим о процессе обновления данных, который является более сложным.

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

IQueryable<T> и магия деревьев выражений


В первом уроке мы создали службу домена, которая выглядела приблизительно так:

public IQueryable<Task> GetTasks()
 {
     return this.ObjectContext.Tasks;
 }

Если немного подумать о том, как он работает, то станет ясно, что происходит извлечение всей таблицы из БД при каждом вызове метода.

Однако это только на первый взгляд. Давайте разберемся, что же такое деревья выражений и отложенное выполнение?

Итак. Когда происходит вызов метода «GetTasks» — это не означает, что происходит запрос к БД и извлекаются данные. На самом деле происходит всего-навсего построение дерева выражений и возврат его как IQueryable, которое просто описывает, что может быть возвращено этим методом. В данном случае, теоретически есть возможность получить всю таблицу Tasks. Так же дерево выражений описывает то, что может быть возвращено клиентской стороне. Непосредственно выполнение запроса и процесс извлечения данных из БД происходит лишь в тот момент, когда что-то пытается изменять коллекцию, которую предоставляет/описывает дерево выражений. Однако остается возможность изменения дерева выражений получателем после того, как запрос отправлен, а это в свою очередь может изменить и результаты, которыми в итоге заполнится коллекция.

Возможность изменения дерева выражений присутствует и непосредственно перед процессом извлечений данных. Например:

TasksDomainContext context = new TasksDomainContext();
 taskDataGrid.ItemsSource = context.Tasks;
 EntityQuery<Task> query = context.GetTasksQuery();
 LoadOperation<Task> loadOp = context.Load(query.Where(t=>t.TaskId == 1));

Во второй строчке происходит привязка коллекции Tasks через контекст домена, который на самом деле еще пустой, так как происходит формирование контекста домена. Затем происходит получение EntityQuery из контекста. Тут еще нет непосредственного выполнения запроса и извлечения данных из БД. Однако EntityQuery позволяет сформировать дерево выражений, на основе которого на сервере, после вызова метода будет сформирован запрос к БД и произойдет извлечение данных. В данном случае возможно извлечение всей таблицы. И только при вызове метода «Load» происходит передача измененного дерева выражений, который включает фильтр «Where», а после отработки запроса будет возвращена только одна строка, у которой в столбце «ID» будет значение «1». Ппроизойдет асинхронный вызов к серверной части, передастся дерево выражений и произойдет извлечение. Однако даже на серверной части запрос уже будет модифицирован и из БД будет возвращена всего одна строка. Вы можете убедиться в этом сами, просто просмотрев, какие SQL запросы будут выполнены при вызове этого метода. То есть создание отдельного метода для извлечения нескольких строк и одной строки из БД отпадает, что облегчает жизнь программисту.

Кэширование в DomainContext и отслеживание изменений

Логику работы разобрали. Но перед тем, как перейдем к разбору кода необходимо разложить по полочкам еще и некоторые концепции работы WCF RIA Services. Вышесказанное далеко не все, что происходит за кулисами контекста домена. Например, имеют место быть вызов прокси. Любые сущности или их коллекции, которые Вы получаете кэшируются контекстом домена на стороне клиента. Именно по этой причине появляется возможность, как в примере выше, привязать ItemsSource к коллекции Tasks до непосредственного выполнения запроса. Данная коллекция будет изменена на актуальную, а данные в UI автоматически обновятся в тот момент, когда придет ответ после асинхронного вызова к серверу. В дополнение к кэшированию, контекст домена хранит информацию о любых изменениях кэшированной сущности, и поэтому всегда знает, если происходит изменение, удаление или добавление.

На основе всего, что мы уже узнали, можно сделать вывод, что не нужно каждый раз делать вызовы серверной части при любом изменении объекта. Можно, например, накопить изменения объектов а потом один раз осуществить вызов серверной части и все изменения будут правильно внесены и обработаны.

Шаг 1: Добавление метода обновления данных в службу домена.

В первом уроке, при создании службы домена мы воспользовались мастером создания. И если поставить галочку напротив каждой сущности в столбце «Добавить редактирование», то получим автоматически сгенерированные методы для каждой сущности, реализующие функционал CRUD.

WCF RIA Services. Обновление данных. Часть 3

Выглядеть код будет следующим образом:

public void InsertTask(Task task)
 {
     if ((task.EntityState != EntityState.Detached))
     {
         this.ObjectContext.ObjectStateManager.ChangeObjectState(task, EntityState.Added);
     }
     else
     {
         this.ObjectContext.Tasks.AddObject(task);
     }
 }
  
 public void UpdateTask(Task currentTask)
 {
     this.ObjectContext.Tasks.AttachAsModified(currentTask, this.ChangeSet.GetOriginal(currentTask));
 }
  
 public void DeleteTask(Task task)
 {
     if ((task.EntityState == EntityState.Detached))
     {
         this.ObjectContext.Tasks.Attach(task);
     }
     this.ObjectContext.Tasks.DeleteObject(task);
 }

Эти методы являются простыми обертками над соответствующими операциями entity framework.

Шаг 2: Добавление элементов в UI для новых действий

Добавим две кнопки, которые будут добавлять новое задание и сохранять измененную коллекцию в БД.

<Button Name="addTaskButton" Content="Add Task" Click="addTaskButton_Click" .../>
 <Button Name="saveChangesButton" Content="Save Changes" Click="saveChangesButton_Click" .../>

WCF RIA Services. Обновление данных. Часть 3

Шаг 3: Создаем новое задание, добавление его в контекст домена и сохранение изменений.

Добавим следующий код в обработчики событий «Click» новых кнопок соответственно:

TasksDomainContext context = new TasksDomainContext();
  
 private void addTaskButton_Click(object sender, RoutedEventArgs e)
 {
     taskDataGrid.ItemsSource = context.Tasks;
     context.Load(context.GetTasksQuery());
     Task newTask = new Task 
     { 
         TaskName = "Deploy app", 
         Description = "Deploy app to all servers in data center", 
         StartDate = DateTime.Today, 
         EndDate = DateTime.Today + TimeSpan.FromDays(7) 
     };
     context.Tasks.Add(newTask);
 }
  
 private void saveChangesButton_Click(object sender, RoutedEventArgs e)
 {
     context.SubmitChanges();
 }

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

После нажатия на кнопку «Add Task», его обработчик замещает ItemsSource нашего DataGrid, что б заместить DomainDataSource, который мы подключили в первом уроке. Затем вызывается метод «Load», что б заполнить контекст домена нужной сущностью.
Далее создаем и заполняем новый объект Tasks и добавляем его к коллекции контекста домена Tasks. Эти изменения сразу же отобразятся в UI, так как упомянутая коллекция реализует интерфейс INotifyCollectionChanged. Но, учтите, что все эти изменения применились, отобразились и сохранились в кэше контекста домена. Но не были изменены на серверной части и в БД. Для применения изменений необходимо вызвать метод SubmitChanges, который и вызывается при нажатии на соответствующую кнопку нашего приложения.

Когда Вы нажмете на кнопку «Add Task» — увидите, что добавилось новое задание, однако в поле «TaskId» значения всегда будут «0». Однако если нажать на кнопку «SubmitChanges», то через некоторое время, а именно после того как произойдет асинхронный вызов, выполнится запрос и вернутся данные обновятся и станут актуальными.

Асинхронное API контекста домена

Я уже это упоминал, но повторюсь еще разок. Такие методы как «Load» и «SubmitChanges» API контекста домена вызываются асинхронно. Это означает, что они не тормозят работу вызывающего их потока, в котором обычно находится UI. Они берут поток из пула потоков «за кулисами», делают вызов серверной части в фоновом режиме, и когда вызов отрабатывает, возвращаются в вызывающий поток UI и обновляют коллекцию сущностей и непосредственно сам UI.

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

Первый вариант заключается в работе с возвращаемым значением от асинхронно вызываемого метода. Метод Load возвращает LoadOperation, а метод SubmitChanges возвращает SubmitOperation. Они оба наследуют OperationBase и в силу этого предоставляют достаточное количество информации об операции, которую Вы можете использовать в процессе работы или после завершения операции. Так же они вызывают событие «Completed» по окончанию операции, и естественно у Вас есть возможность подписаться на это событие. Само собой доступны различные ошибки, срабатывания различных флагов и многое другое, что можно использовать при создании приложения.

Как альтернативу на подписку события «Completed» можно использовать вызов перегруженного метода «Load» или «SubmitChanges», которые возвращают соответственно Action и Action. Передаете ссылку на функцию обратного вызове и при завершении операции он автоматически вызывается.

Видео для этого урока

Исходники

На Github

Автор: struggleendlessly

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js