我正在构建一个使用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端绑定我的数据,也可以在我的模型端以线程安全的方式绑定。
建议?
谢谢!
答案 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将不必更改。
思考,评论和建议?