Какой правильный узор? Наблюдатель между моделью представления и сервисом

Я создаю клиентское приложение WP7, которое взаимодействует с веб-службой (например, SOAP) с использованием Mvvm-Light.

У меня есть ViewModel, которая реализует INotifyPropertyChanged и вызывает RaisePropertryChanged с установленным широковещательным флагом.

И мое представление (XAML), и моя модель (которая выполняет HTTP-запросы к веб-службе) подписываются на изменения свойств. XAML, очевидно, из-за INotifyPropertyChanged и моей модели, позвонив

Messenger.Default.Register<SysObjectCreatedMessage>(this, (action) => SysObjectCreatedHandler(action.SysObject));

Боюсь, этот шаблон не сработает из-за следующего:

Когда я получаю данные из веб-службы, я устанавливаю свойства своей ViewModel (используя DispatcherHelper.CheckBeginInvokeUI). На самом деле я использую Reflection, и мой вызов выглядит так:

GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() => pinfo.SetValue(this, e.Response, null));

Вот в чем проблема: результирующий набор свойств, вызванный этим вызовом SetValue, заставляет мое свойство set вызывать RaisePropertryChanged, заставляя меня отправлять данные, которые я только что получил с сервера, обратно на него.

EDIT — Добавление дополнительного контекста по предложению Пера Джона

Вот некоторые из моих 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. Он обертывает Http POST и обрабатывает, заставляя сервер время от времени «запрашивать» последние данные. Я планирую в конечном итоге реализовать уведомления, но пока я голосую. Он использует немного отражения для динамического преобразования результатов 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, а также на стороне моей модели потокобезопасным способом.

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

Спасибо!


person tig    schedule 14.09.2010    source источник


Ответы (2)


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

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

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

person Jon Skeet    schedule 14.09.2010
comment
@cek: Это все еще не объясняет, почему вы хотите, чтобы модель запускала новый запрос только потому, что ViewModel изменилась ... похоже, что ваша модель подписки в основном сломана. Я не думаю, что это действительно проблема с потоками... это проблема с подпиской. Подумайте, что должно на самом деле заставить вашу модель выполнить еще один запрос. - person Jon Skeet; 14.09.2010
comment
Я согласен с Джоном. Нет необходимости использовать Messenger для связи между вашей ViewModel и вашей моделью. Ваша ViewModel может вызывать методы непосредственно в вашей модели. Вам следует подумать о том, чтобы ваша модель вызывала события после завершения вызовов службы, а ваша ViewModel обрабатывала эти события. - person Matt Casto; 14.09.2010

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

Резюмируя вопрос еще раз:

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

Наиболее важным методом для 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 (свойство value). Отображаемое имя выглядит так:

    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 имеет значение True, он также отправляется обратно на сервер.

Так как же устанавливается .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 не должны будут меняться.

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

person tig    schedule 21.01.2011