Какой правильный шаблон? Наблюдатель между моделью и сервисом
Я создаю клиентское приложение WP7, которое взаимодействует с веб-сервисом (как SOAP), используя Mvvm-Light.
У меня есть ViewModel, который реализует оба INotifyPropertyChanged
и звонки RaisePropertryChanged
с установленным флагом трансляции.
И мое представление (XAML), и моя модель (которая выполняет HTTP-запросы к веб-службе) подписываются на изменения свойств. XAML, очевидно, из-за INotifyPropertyChanged
и моя модель по телефону
Messenger.Default.Register<SysObjectCreatedMessage>(this, (action) => SysObjectCreatedHandler(action.SysObject));
Боюсь, этот шаблон не сработает из-за следующего:
Когда я получаю данные из веб-сервиса, я устанавливаю свойства в моей модели представления (используя DispatcherHelper.CheckBeginInvokeUI
). Я на самом деле использую Reflection, и мой вызов выглядит так:
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() => pinfo.SetValue(this, e.Response, null));
Вот проблема: результирующий набор свойств, вызванный этим вызовом SetValue, вызывает мое свойство set
звонить RaisePropertryChanged
заставляя меня отправлять данные, которые я только что получил с сервера, обратно на него.
РЕДАКТИРОВАТЬ - Добавление большего контекста в соответствии с предложением Джона
Вот некоторые из моих XAML. Мой класс GarageDoorOpener имеет свойство GarageDoorOpened
На сервере управления домом есть куча объектов гаражных ворот, которые имеют логическое свойство, показывающее, открыты они или нет. Я могу получить к ним доступ, используя HTTP POST формы:
http: // server / sys / Home / Наверху / Гараж / Западная дверь гаража?f??GarageDoorOpened
Полученное в результате тело HTTP будет содержать либо True, либо False.
Та же модель применяется к другим объектам в доме с другими типами (строки, целые числа и т. Д.).
На данный момент я просто фокусируюсь на открывателях гаражных ворот.
Модель View для гаражных ворот выглядит так:
public class GarageDoorSensor : SysObject
{
public static new string SysType = "Garage Door Sensor";
public const string GarageDoorOpenedPropertyName = "GarageDoorOpened";
public Boolean _GarageDoorOpened = false;
[SysProperty]
public Boolean GarageDoorOpened
{
get
{
return _GarageDoorOpened;
}
set
{
if (_GarageDoorOpened == value)
{
return;
}
var oldValue = _GarageDoorOpened;
_GarageDoorOpened = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(GarageDoorOpenedPropertyName, oldValue, value, true);
}
}
}
Класс SysObject, от которого он наследуется, выглядит следующим образом (упрощенно):
public class SysObject : ViewModelBase
{
public static string SysType = "Object";
public SysObject()
{
Messenger.Default.Send<SysObjectCreatedMessage>(new SysObjectCreatedMessage(this));
}
}
protected override void RaisePropertyChanged<T>(string propertyName, T oldValue, T newValue, bool broadcast)
{
// When we are initilizing, do not send updates to the server
// if (UpdateServerWithChange == true)
// ****************************
// ****************************
//
// HERE IS THE PROBLEM
//
// This gets called whenever a property changes (called from set())
// It both notifies the "server" AND the view
//
// I need a pattern for getting the "SendPropertyChangeToServer" out
// of here so it is only called when properties change based on
// UI input.
//
// ****************************
// ****************************
SendPropertyChangeToServer(propertyName, newValue.ToString());
// Check if we are on the UI thread or not
if (App.Current.RootVisual == null || App.Current.RootVisual.CheckAccess())
{
base.RaisePropertyChanged(propertyName, oldValue, newValue, broadcast);
}
else
{
// Invoke on the UI thread
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() =>
base.RaisePropertyChanged(propertyName, oldValue, newValue, broadcast));
}
}
private void SendPropertyChangeToServer(String PropertyName, String Value)
{
Messenger.Default.Send<SysObjectPropertyChangeMessage>(new SysObjectPropertyChangeMessage(this, PropertyName, Value));
}
// Called from PremiseServer when a result has been returned from the server.
// Uses reflection to set the appropriate property's value
public void PropertySetCompleteHandler(HttpResponseCompleteEventArgs e)
{
// BUGBUG: this is wonky. there is no guarantee these operations will modal. In fact, they likely
// will be async because we are calling CheckBeginInvokeUI below to wait on the UI thread.
Type type = this.GetType();
PropertyInfo pinfo = type.GetProperty((String)e.context);
// TODO: Genericize this to parse not string property types
//
if (pinfo.PropertyType.Name == "Boolean")
{
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() => pinfo.SetValue(this, Boolean.Parse(e.Response), null));
//pinfo.SetValue(this, Boolean.Parse(e.Response), null);
}
else
{
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() => pinfo.SetValue(this, e.Response, null));
//pinfo.SetValue(this, e.Response, null);
}
}
}
Моя "модель" называется PremiseServer. Он оборачивает POST Http и обрабатывает, заставляя сервер "запрашивать" последние данные время от времени. Я планирую в конечном итоге реализовать уведомления, но сейчас я опрашиваю. Он использует немного Reflection для динамического перевода результатов HTTP в наборы свойств. По своей сути это выглядит так (я очень горжусь собой за это, хотя мне, вероятно, вместо этого должно быть стыдно).
protected virtual void OnRequery()
{
Debug.WriteLine("OnRequery");
Type type;
foreach (SysObject so in sysObjects)
{
type = so.GetType();
PropertyInfo[] pinfos = type.GetProperties();
foreach (PropertyInfo p in pinfos)
{
if (p.IsDefined(typeof(SysProperty),true))
SendGetProperty(so.Location, p.Name, so, so.PropertySetCompleteHandler);
}
}
}
protected delegate void CompletionMethod(HttpResponseCompleteEventArgs e);
protected void SendGetProperty(string location, string property, SysObject sysobject, CompletionMethod cm)
{
String url = GetSysUrl(location.Remove(0, 5));
Uri uri = new Uri(url + "?f??" + property);
HttpHelper helper = new HttpHelper(uri, "POST", null, true, property);
Debug.WriteLine("SendGetProperty: url = <" + uri.ToString() + ">");
helper.ResponseComplete += new HttpResponseCompleteEventHandler(cm);
helper.Execute();
}
Обратите внимание, что OnRequery - не единственное место, откуда я в конечном итоге буду вызывать SendGetProperty; это просто там для инициализации лесов на данный момент. Идея заключается в том, что у меня может быть общий фрагмент кода, который получает "сообщение от сервера" и переводит его в вызов SysObject.Property.SetValue()...
КОНЕЦ РЕДАКТИРОВАНИЯ
Мне нужен шаблон, который позволит мне связывать как данные на стороне XAML, так и на стороне модели безопасным для потока способом.
Предложения?
Спасибо!
2 ответа
Я возобновил работу над этим проектом в течение последних нескольких недель и, наконец, нашел решение. Учитывая комментарии и мысли людей, размещенные выше, я не уверен, что кто-либо, кроме меня, понимает, что я пытаюсь сделать, но я подумал, что, возможно, стоит опубликовать, как я решил это. Как минимум, написание этого гарантирует, что я понимаю это:-).
Резюмируя вопрос еще раз:
У меня есть сервер управления домом, который выставляет объекты в моем доме через интерфейс SOAP. Home.LivingRoom.Fireplace
Например, выставляется как:
http://server/Home/LivingRoom/Fireplace?property=DisplayName
http://server/Home/LivingRoom/Fireplace?property=PowerState
Выполнение HTTP GET против них приведет к ответу HTTP, содержащему значение свойства (например, "Камин в гостиной" и "Выкл" соответственно).
Гаражные ворота (например, Home.Garage.EastGarageDoor
) выставляется как:
http://server/Home/Upstairs/EastGarageDoor?property=DisplayName
http://server/Home/Upstairs/EastGarageDoor?property=GarageDoorOpened
http://server/Home/Upstairs/EastGarageDoor?property=Trigger
Здесь у нас есть свойство, которое, если set вызывает действие (Trigger
). Выполнение POST против этого с HTTP-телом "True" приведет к открытию / закрытию двери.
Я создаю приложение WP7 в качестве внешнего интерфейса для этого. Я решил следовать модели Mvvm и использую Mvvm-Light.
В WP7 нет встроенного способа поддержки уведомлений от интерфейсов REST, и я пока не готов создавать свои собственные (хотя это на моем радаре). Поэтому, чтобы пользовательский интерфейс отображал актуальное состояние, мне нужно опросить. Количество сущностей и объем данных относительно невелики, и теперь я доказал, что могу сделать так, чтобы они хорошо работали с опросом, но есть некоторые оптимизации, которые я могу сделать, чтобы улучшить их (в том числе добавление смарт-символов на сервер, чтобы включить систему, аналогичную уведомлениям).).
В моем решении я размыл границу между моей моделью и моей моделью представления. Если вы действительно хотите быть в тени, моя "Модель" - это просто низкоуровневые классы, которые я использую для упаковки моих запросов Http (например, GetPropertyAsync(objectLocation, propertyName, completionMethod)
).
В итоге я определил универсальный класс для свойств. Это выглядит так:
namespace Premise.Model
{
//where T : string, bool, int, float
public class PremiseProperty<T>
{
private T _Value;
public PremiseProperty(String propertyName)
{
PropertyName = propertyName;
UpdatedFromServer = false;
}
public T Value
{
get { return _Value; }
set { _Value = value; }
}
public String PropertyName { get; set; }
public bool UpdatedFromServer { get; set; }
}
}
Затем я создал ViewModelBase
(от Mvvm-Light) производный базовый класс PremiseObject
который представляет базовый класс, на котором основан каждый объект в системе управления (например, который буквально называется "Объект").
Самый важный метод на PremiseObject
это переопределение RaisePropertyChanged
:
/// </summary>
protected override void RaisePropertyChanged<T>(string propertyName, T oldValue, T newValue, bool sendToServer)
{
if (sendToServer)
SendPropertyChangeToServer(propertyName, newValue);
// Check if we are on the UI thread or not
if (App.Current.RootVisual == null || App.Current.RootVisual.CheckAccess())
{
// broadcast == false because I don't know why it would ever be true
base.RaisePropertyChanged(propertyName, oldValue, newValue, false);
}
else
{
// Invoke on the UI thread
// Update bindings
// broadcast == false because I don't know why it would ever be true
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() =>
base.RaisePropertyChanged(propertyName, oldValue, newValue, false));
}
}
Обратите внимание на несколько вещей:
1) Я переигрываю / пересматриваю broadcast
параметр. Если это правда, то изменение свойства "отправляется на сервер" (я делаю HTTP POST). Я не использую изменения свойств вещания где-либо еще (и я даже не уверен, для чего я это использовал).
2) Я всегда передаю трансляцию в False при звонке base.
,
PremiseObject
имеет набор стандартов PremiseProperty
Свойства на нем: Местоположение (URL-адрес объекта), Имя, DisplayName, Значение (свойство value). DisplayName выглядит так:
protected PremiseProperty<String> _DisplayName = new PremiseProperty<String>("DisplayName");
public string DisplayName
{
get
{
return _DisplayName.Value;
}
set
{
if (_DisplayName.Value == value)
{
return;
}
var oldValue = _DisplayName;
_DisplayName.Value = value;
// Update bindings and sendToServer change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(_DisplayName.PropertyName,
oldValue, _DisplayName, _DisplayName.UpdatedFromServer);
}
}
Так что это значит в любое время .DisplayName
изменения в моей программе, он передается на весь пользовательский интерфейс и если и только если _DisplayName.UpdatedFromServer
Верно ли, что оно также отправляется обратно на сервер.
Так как же .UpdatedFromServer
приготовься? Когда мы получаем наш обратный вызов из асинхронного запроса Http:
protected void DisplayName_Get(PremiseServer server)
{
String propertyName = _DisplayName.PropertyName;
_DisplayName.UpdatedFromServer = false;
server.GetPropertyAsync(Location, propertyName, (HttpResponseArgs) =>
{
if (HttpResponseArgs.Succeeded)
{
//Debug.WriteLine("Received {0}: {1} = {2}", DisplayName, propertyName, HttpResponseArgs.Response);
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
DisplayName = (String)HttpResponseArgs.Response; // <-- this is the whole cause of this confusing architecture
_DisplayName.UpdatedFromServer = true;
HasRealData = true;
});
}
});
}
Всякий раз, когда пользовательский интерфейс хочет свежих данных, эти функции XXX_Get вызываются (например, по таймеру опроса, когда изменяется представление, запуск приложения и т. Д.)
Я должен продублировать приведенный выше код для каждого определяемого мной свойства, что довольно болезненно, но я пока не нашел способа его обобщить (поверьте мне, я пробовал, но мои знания C# просто недостаточно сильны, и я просто продолжай двигать проблему). Но это работает, и работает хорошо.
Чтобы охватить все базы, вот пример свойства Trigger класса GarageDoor:
protected PremiseProperty<bool> _Trigger = new PremiseProperty<bool>("Trigger");
public bool Trigger
{
set
{
if (value == true)
RaisePropertyChanged(_Trigger.PropertyName, false, value, true);
}
}
Обратите внимание, как я заставляю broadcast
параметр для RaisePropertyChanged
в true, и как это свойство "Только для записи"? Это создает HTTP-запрос POST по URL-адресу "GarageDoor.Location" + ?propertyName=
+ value.ToString()
,
Я очень доволен этим получилось. Это что-то вроде хака, но теперь я реализовал несколько сложных представлений, и это хорошо работает. Разделение, которое я создал, позволит мне изменить базовый протокол (например, группировать запросы и заставить сервер отправлять только измененные данные), и мои ViewModels не должны будут меняться.
Мысли, комментарии, предложения?
Ну, один из вариантов - сделать вашу ViewModel ответственной за явный вызов модели, а не за использование мессенджера. Таким образом, для ViewModel легче знать, что ему не нужно запускать запрос на это изменение.
Альтернативой для модели является проверка вновь установленного значения, чтобы увидеть, соответствует ли оно собственному представлению о "текущем" значении. Вы действительно не сказали нам, что здесь происходит с точки зрения того, что представляет собой ответ или что ищет сервер, но обычно я ожидаю, что это будет случай проверки того, равно ли старое значение новому значению, и игнорируя "изменение", если так.
Если бы вы могли показать короткий, но полный пример всего этого, было бы легче об этом говорить.