Introduction
Take the following scenario: A user of your application opens a customer for editing. They put in a new address. Now they realize they're in the wrong customer entirely and want to cancel their changes.
What does your application do? If you're like me, then you are on a mission to keep code behind to an absolute minimum. You have two way bindings setup for everything. You're proud of all the C#/VB code that you haven't written in your application. So, your model is already updated. Do you not have a cancel button? Blame the user for the bad experience and tell them not to do that? Or did you add a reload method to your cancel button instead, forcing the application to grab data from a web service or a database?
Another possible option might have been to follow MVVM completely and to separate the Model from the ViewModel even on the most basic classes. The ViewModel could then store copies of this data and include methods to move back to the object and to reload data from the object. The downside here is that you need to add properties twice, and always remember to include them in your methods. This is a lot of code duplication.
Here's another predicament: You use a web service for your data source. You'd like to keep the data sent across the Internet to a minimum. But your class has 100+ properties. Sure, only one might have changed, but how do you know which ones have changed? Do you serialize the entire class to send it? Do you create 100 variables called OrgAddress, OrgName, etc.?
I'd like to share a solution to both of these that has worked for me: A special base class.
Using the Code
Let's introduce our class:
public class ChangeSetClass : IDisposable, INotifyPropertyChanged
It optionally implements two interfaces:
IDisposable for using
using() statements and
InotifyPropertyChanged so that our simple classes will not need a separate
ViewModel. I'm not saying we should eliminate the
ViewModel. For MANY cases, I still use it. But when I have a class that only contains basic properties, I have gone this route.
Note also that even if you do implement INotifyPropertyChanged, you can still use a separate ViewModel with this. You just don't raise any notifications in your object, and instead do that in your actual ViewModel.
Now, let's introduce a property and a private variable to our class. ParentType will remember what type the originating object really is, and OrgList will maintain a set of values.
public Type ParentType;
private List<Object> OrgList = new List<object>();
Next, let's define a customer class for our example:
public class Customer : ChangeSetClass
{
public int ID { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public Customer()
{
base.ParentType = typeof(Customer);
}
}
So, in our constructor, we set the ParentType in our ChangeSetClass class. Now the magic can begin. We introduce new methods to the ChangeSetClass:
public void InitializeValues()
This is called when the object is loaded from our source. It simply enumerates through the properties in the class and adds them to the OrgList.
public void ResetValues()
This is called when the user attempts to cancel something. The OrgList values are put back into the object.
public bool HasChanged()
This loops the properties and compares them to the OrgList letting us know if anything has changed.
All three methods here are similar in nature, so let's just take a look at the first one:
public void InitializeValues()
{
PropertyInfo[] properties = ParentType.GetProperties();
OrgList.Clear();
for (int i = 0; i < properties.GetLength(0); i++)
{
if (properties[i].CanWrite)
{
OrgList.Add(properties[i].GetValue(this, null));
}
}
}
This method uses reflection to look up our object hierarchy. It will work for any object we throw at it. We could also account for nested classes or other data types. Or, if desired, we could enumerate any IEnumerable property. So if my class is an Invoice class, and I have a List<InvoiceLines> property inside it, the HasChanged() method could dig down into this list too. Better yet, why not make the same three methods with an optional parameter (bool bRecusive)?
Let's look at some benefits of this design:
- All bindings work as expected.
- If there is an intermediate
ViewModel, it can write directly to the object while performing notification, and does not need to store separate variables, nor implement methods to move data into the model or out of the model.
- Since updates are live to the object, the screen my modal customer edit window is partially covering is being updated as the user works.
- All new classes based on
ChangeSetClass will automatically inherit these properties and be able to rollback changes when needed. No special ViewModels are needed.
- Any new property added to our class is instantly seen by the
ChangeSetClass because of Reflection.
Now let's look at our second predicament from before. In web based applications, you would want to transmit the least amount of data possible. So, why not let the ChangeSetClass do this work for you?
public string ChangesXML { get { return GetChangesXML(); } }
private string GetChangesXML()
{
PropertyInfo[] properties = ParentType.GetProperties();
TextWriter tw = new StringWriter();
using (XmlWriter writer = XmlWriter.Create(tw))
{
writer.WriteStartDocument();
writer.WriteStartElement(ParentType.ToString());
int iy = -1;
bool bChanges = false;
for (int i = 0; i < OrgList.Count; i++)
{
if (properties[i].CanWrite)
{
iy++;
if (OrgList.Count > iy)
{
if (((OrgList[iy] == null) &&
(properties[i].GetValue(this, null) != null)) ||
((OrgList[iy] != null) && (properties[i].GetValue
(this, null) == null)) ||
((OrgList[iy] != null) && (properties[i].GetValue
(this, null) != null) &&
(!(OrgList[iy].Equals(properties[i].GetValue(this, null))))))
{
bChanges = true;
writer.WriteElementString(properties[i].Name,
properties[i].GetValue(this, null).ToString());
}
}
}
}
if (!bChanges) return string.Empty;
writer.WriteEndElement();
writer.WriteEndDocument();
}
return tw.ToString();
}
This provides XML of only the properties that have changed. This is minimal data to send to a web service.
In conclusion, we have looked at a possible solution for solving the two way binding conundrum and allowing for changes to rollback, and we've also created a standardized format for updating our web service, should we be using one. I believe this standardization will help to produce future classes more rapidly and with better consistency.
History
- 05-04-2011: Initial version