Какой правильный шаблон?Наблюдатель между моделью и сервисом - PullRequest
0 голосов
/ 14 сентября 2010

Я создаю клиентское приложение 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/Upstairs/Garage/West Дверь гаража? F ?? GarageDoorOpened

Полученное в результате тело HTTP будет содержать True или False.

Та же модель применима и к другим предметам в доме с другими типами (строки, целые числа и т. Д.).

Пока я просто концентрируюсь на открывателях гаражных ворот.

Модель представления для ворот гаража выглядит следующим образом:

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 () ...

END EDIT

Мне нужен шаблон, который позволит мне связывать мои данные как на стороне XAML, так и на стороне моей модели потокобезопасным способом.

Предложения?

Спасибо!

Ответы [ 2 ]

1 голос
/ 21 января 2011

Я возобновил работу над этим проектом в течение последних нескольких недель и, наконец, нашел решение.Учитывая комментарии и мысли людей, размещенные выше, я не уверен, что кто-либо, кроме меня, понимает, что я пытаюсь сделать, но я подумал, что, возможно, стоит опубликовать, как я решил это.Как минимум, его написание гарантирует, что I поймет это: -).

Обобщая вопрос еще раз:

У меня есть сервер управления домом, который выставляет объекты в моемдомой через интерфейс 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

Здесь мыесть свойство, которое, если установлено, вызывает действие (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, который представляет базовый класс, на котором основан каждый объект в системе управления (например, которыйбуквально называется `Object ').

Самый важный метод в 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: Location (URL-адрес объекта), Name, 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);
        }
    }

Обратите внимание, как заставить параметр broadcastRaisePropertyChanged в true, и как это свойство "Только запись"?В результате создается HTTP-запрос POST по URL-адресу «GarageDoor.Location» + ?propertyName= + value.ToString().

Я очень доволен тем, что получилось. Это что-то вроде хака, но теперь я реализовал несколько сложных представлений, и это хорошо работает. Разделение, которое я создал, позволит мне изменить базовый протокол (например, группировать запросы и заставить сервер отправлять только измененные данные), и мои ViewModels не должны будут меняться.

Мысли, комментарии, предложения?

1 голос
/ 14 сентября 2010

Что ж, один из вариантов - сделать вашу ViewModel ответственной за явный вызов модели, а не за использование мессенджера.Таким образом, для ViewModel легче знать, что ему не нужно запускать запрос на это изменение.

Альтернативой для модели является проверка вновь установленного значения, чтобы увидеть, соответствует ли оно или нетсвое представление о «текущей» стоимости.Вы действительно не сказали нам, что здесь происходит с точки зрения того, что представляет собой ответ или что ищет сервер, но обычно Я бы ожидал, что это будет случай проверки того, равно ли старое значениек новому значению и игнорируя «изменение», если так.

Если бы вы могли показать краткий, но полный пример всего этого, было бы легче говорить об этом.

...