I am building a WP7 client app that talk to a web service (SOAP like) using Mvvm-Light.
I have a ViewModel that both implements INotifyPropertyChanged
and calls RaisePropertryChanged
with the broadcast flag set.
Both my view (XAML) and my model (which does HTTP requests to the web service) subscribe to property changes. XAML, obviously, because of INotifyPropertyChanged
, and my model by calling
Messenger.Default.Register<SysObjectCreatedMessage>(this, (action) => SysObjectCreatedHandler(action.SysObject));
This pattern is not going to work, i'm afraid, because of the following:
When I get data back from the web service I set properties on my ViewModel (using DispatcherHelper.CheckBeginInvokeUI
). I actually use Reflection and my call looks like this:
GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(() => pinfo.SetValue(this, e.Response, null));
Here's the problem: The resultant property set caused by that call to SetValue causes my property set
to call RaisePropertryChanged
causing me to send the data I just got from the server right back to it.
EDIT - Adding more context Per Jon's suggestion
Here's some of my XAML. My GarageDoorOpener class has a GarageDoorOpened property
On the home control server there are a bunch of garage door objects that have a boolean property representing whether they are open or not. I can access these using HTTP POSTs of the form:
http://server/sys/Home/Upstairs/Garage/West Garage Door?f??GarageDoorOpened
The the resultant HTTP body will contain either True or False.
The same model applies to other objects in the home with other types (Strings, Integers, etc...).
For now I'm just focusing on the garage door openers.
The View Model for the garage door looks like this:
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 oldValu开发者_JS百科e = _GarageDoorOpened;
_GarageDoorOpened = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(GarageDoorOpenedPropertyName, oldValue, value, true);
}
}
}
The SysObject class this inherits from looks like this (simplified):
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);
}
}
}
My "model" is called PremiseServer. It wraps Http POSTs and handles causing the server to be "queried" for the latest data every once and a while. I plan on eventually implementing notifications, but for now i am polling. It uses a bit of Reflection to dynamically translate the HTTP results into property sets. At it's core it looks like this (i'm pretty proud of myself for this, although I probably should be ashamed instead).
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();
}
Note that OnRequery is not the only place I'll eventually be calling SendGetProperty from; it's just there for initialization scaffolding for now. The idea is that I can have a generic piece of code that gets a "message from the server" and translates it into a SysObject.Property.SetValue() call...
END EDIT
I need a pattern that will let me both bind to my data on the XAML side but also on my model side in a thread safe way.
Suggestions?
Thanks!
Well, one option is to make your ViewModel responsible for explicitly calling on the model, rather than using the messenger. That way it's easier for the ViewModel to know that it doesn't need to fire off a request for this change.
The alternative is for the model to check the newly set value to see whether or not it corresponds with its own idea of the "current" value. You haven't really told us what's going on here in terms of what the response is or what the server's looking for, but usually I'd expect this to be a case of checking whether an old value is equal to a new value, and ignoring the "change" if so.
If you could show a short but complete example of all of this, it would make it easier to talk about though.
I re-engaged on this project over the last few weeks and finally came up with a solution. Given the comments and thoughts people posted above, I'm not sure anyone but me understands what I'm trying to do, but I figured it might be worthwhile posting how I solved this. At the minimum, writing it up will ensure I understand it :-).
Summarizing the question again:
I have a home control server that exposes objects in my home via a SOAP interface. Home.LivingRoom.Fireplace
, for example is exposed as:
http://server/Home/LivingRoom/Fireplace?property=DisplayName
http://server/Home/LivingRoom/Fireplace?property=PowerState
Doing an HTTP GET against these will result in the HTTP reply containing the property value (e.g. "Living Room Fireplace" and "Off" respectively).
A garage door (e.g. Home.Garage.EastGarageDoor
) is exposed as:
http://server/Home/Upstairs/EastGarageDoor?property=DisplayName
http://server/Home/Upstairs/EastGarageDoor?property=GarageDoorOpened
http://server/Home/Upstairs/EastGarageDoor?property=Trigger
Here we have a property that if set causes an action (Trigger
). Doing a POST against this with a HTTP body of "True" will cause the door to open/close.
I am building a WP7 app as a front end to this. I have decided to follow the Mvvm model and am using Mvvm-Light.
WP7 does not have a built in way of supporting notifications from REST interfaces, and I am not yet ready to build my own (although it is on my radar). So for the UI to show up-to-date state I need to poll. The # of entities & amount of data is relatively small and I have now proven that I can make it work well with polling, but there are some optimizations I can do to improve it (including adding smarts to the server to enable a notifcation like system).
In my solution I've blurred the lines between my model & my viewmodel. If you really wanted to be pendantic about it my "Model" is just the low level classes I have for wrapping my Http requests (e.g GetPropertyAsync(objectLocation, propertyName, completionMethod)
).
What I ended up doing is defining a generic class for properties. It looks like this:
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; }
}
}
I then created a ViewModelBase
(from Mvvm-Light) derived base class PremiseObject
which represents the base class each object in the control system is based on (e.g. which is literally called `Object').
The most important method on PremiseObject
is it's override of 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));
}
}
Note a few things:
1) I am over-riding/re-purposing the broadcast
parameter. If it is True then a the property change is "sent to the server" (I do an HTTP POST). I don't use broadcast property changes anywhere else (and I'm not actually even sure what I would use it for).
2) I always pass broadcast to False when calling base.
.
PremiseObject
has a set of standard PremiseProperty
properties on it: Location (the URL to the object), Name, DisplayName, Value (the value property). DisplayName looks like this:
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);
}
}
So this means anytime .DisplayName
changes within my program it gets relayed to all UI and IF AND ONLY IF _DisplayName.UpdatedFromServer
is True does it also get sent back to the server.
So how does .UpdatedFromServer
get set? When we get our callback from an async Http request:
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;
});
}
});
}
Whenever the UI wants fresh data these XXX_Get functions get called (e.g. on a polling timer, when a view changes, app start, etc...)
I have to duplicate the code above for each property I define which is pretty painful, but I have not yet found a way to genericize it (trust me, I've tried, but my knowledge of C# is just not strong enough and I just keep moving the problem). But this works, and works well.
To cover all bases, here's an example of the Trigger property on the GarageDoor class:
protected PremiseProperty<bool> _Trigger = new PremiseProperty<bool>("Trigger");
public bool Trigger
{
set
{
if (value == true)
RaisePropertyChanged(_Trigger.PropertyName, false, value, true);
}
}
Note how I force the broadcast
parameter to RaisePropertyChanged
to true and how this is a "Write only" property? This generates an HTTP POST against the 'GarageDoor.Location' URL + ?propertyName=
+ value.ToString()
.
I'm pretty happy with this turned out. It's a bit of a hack, but I've now implemented several sophisticated views and it works well. The separation I've created will allow me to change the underling protocol (e.g. batching up requests and having the server only send changed data) and my ViewModels will not have to change.
Thoughts, comments, suggestions?
精彩评论