什么是正确的模式?视图模型和视图之间的观察者服务

时间:2010-09-14 05:13:22

标签: c# design-patterns windows-phone-7 mvvm-light

我正在构建一个使用Mvvm-Light与Web服务(类似SOAP)通信的WP7客户端应用程序。

我有一个ViewModel,它实现INotifyPropertyChanged并调用RaisePropertryChanged并设置广播标志。

我的视图(XAML)和我的模型(对Web服务发出HTTP请求)都订阅了属性更改。显然,XAML是因为INotifyPropertyChanged,而我的模型是通过调用

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

这种模式不会起作用,我担心,因为以下几点:

当我从Web服务获取数据时,我在ViewModel上设置了属性(使用DispatcherHelper.CheckBeginInvokeUI)。我实际上使用Reflection,我的调用如下:

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

问题在于:由对SetValue的调用导致的结果属性集导致我的属性set调用RaisePropertryChanged,导致我将刚刚从服务器获得的数据发送回它。

编辑 - 根据Jon的建议添加更多上下文

这是我的一些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端绑定我的数据,也可以在我的模型端以线程安全的方式绑定。

建议?

谢谢!

2 个答案:

答案 0 :(得分:1)

好吧,一个选项是让ViewModel负责显式调用模型,而不是使用messenger。这样,ViewModel就可以更轻松地知道它不需要触发对此更改的请求。

替代方法是模型检查新设置的值以查看它是否与其自身对“当前”值的概念相对应。您还没有真正告诉我们这里的响应是什么或服务器正在寻找什么,但通常我希望这是一个检查旧值是否为等于一个新值,如果是,则忽略“改变”。

如果您可以展示所有这一切的简短但完整的示例,那么它会更容易讨论。

答案 1 :(得分:1)

过去几周我重新参与了这个项目,最后提出了一个解决方案。鉴于上面发表的评论和想法,我不确定除了我之外的任何人都理解我正在尝试做什么,但我认为发布我如何解决这个问题可能是值得的。至少,编写它将确保理解它: - )。

再次总结一下这个问题:

我有一个家庭控制服务器,通过SOAP接口公开我家中的对象。例如Home.LivingRoom.Fireplace公开为:

http://server/Home/LivingRoom/Fireplace?property=DisplayName http://server/Home/LivingRoom/Fireplace?property=PowerState

对这些进行HTTP GET将导致HTTP回复包含属性值(例如分别为“Living Room Fireplace”和“Off”)。

车库门(例如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)。使用HTTP正文“True”对此进行POST将导致门打开/关闭。

我正在构建一个WP7应用程序作为前端。我决定遵循Mvvm模型并使用Mvvm-Light。

WP7没有内置的方式来支持来自REST接口的通知,我还没有准备好构建我自己的(尽管它在我的雷达上)。因此,UI要显示最新状态,我需要轮询。实体数量&amp;数据量相对较小,我现在已经证明我可以使其与轮询一起运行良好,但我可以做一些优化来改进它(包括向服务器添加智能以启用类似系统的通知)。

在我的解决方案中,我模糊了我的模型和模型之间的界限。我的viewmodel。如果你真的想成为它的pendantic我的“模型”只是我用来包装我的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参数。如果是True,则属性更改“发送到服务器”(我执行HTTP POST)。我不会在其他任何地方使用广播属性更改(我实际上甚至不确定我会用它做什么)。 2)在调用base.时,我总是将广播传递给False。

PremiseObject上有一组标准PremiseProperty属性:位置(对象的URL),Name,DisplayName,Value(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更改它会被转发到所有UI IF并且仅在_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;
                });
            }
        });
    }

每当UI想要新数据时,都会调用这些XXX_Get函数(例如,在轮询计时器,视图更改,应用程序启动等等)

我必须为我定义的每个属性复制上面的代码,这非常痛苦,但我还没有找到一种方法来对它进行泛化(相信我,我已经尝试过了,但我对C#的了解还不够强大我只是继续解决问题)。但这很有效,效果很好。

为了涵盖所有基础,这里是GarageDoor类的Trigger属性的示例:

    protected PremiseProperty<bool> _Trigger = new PremiseProperty<bool>("Trigger");
    public bool Trigger
    {
        set
        {
            if (value == true)
                RaisePropertyChanged(_Trigger.PropertyName, false, value, true);
        }
    }

请注意我如何将broadcast参数强制为RaisePropertyChanged为true以及这是一个“只写”属性?这会针对“GarageDoor.Location”网址+ ?propertyName= + value.ToString()生成HTTP POST。

我很满意这一点。这有点像黑客,但我现在已经实现了几个复杂的视图,它运作良好。我创建的分离将允许我更改底层协议(例如,批量处理请求并让服务器仅发送更改的数据),我的ViewModel将不必更改。

思考,评论和建议?