silverlight: How to implement dynamic object creation in POCO class properties? - silverlight

We're porting our data model from WPF over to Silverlight. On the WPF side, every element of the data model supports dynamic initialization so that we don't have to worry about any bookkeeping. For example:
public UnitNameClass UnitName
{
get
{
if ((this.unitNameField == null))
{
this.unitNameField = new UnitNameClass();
}
return this.unitNameField;
}
set
{
if (((this.unitNameField == null)
|| (this.unitNameField.Equals(value) != true)))
{
this.unitNameField = value;
this.OnPropertyChanged("UnitName"));
}
}
}
The majority of the classes are generated for us, from an XSD representing the data model. There are some other extension classes we've added which are not in the XSD files.
Silverlight generates properties of the following form for its proxy classes:
public UnitNameClass UnitName
{
this.OnCreated();
}
public UnitNameClass UnitName
{
get
{
return this._unitName;
}
set
{
if ((this._unitName != value))
{
this.OnUnitNameChanging(value);
this.RaiseDataMemberChanging("UnitName");
this.ValidateProperty("UnitName", value);
this._unitName = value;
this.RaiseDataMemberChanged("UnitName");
this.OnUnitNameChanged();
}
}
}
Obviously, we're meant to put initialization of all the properties in the OnCreated partial function, but we'd much rather have the ability to add some logic to the get property.
Has anyone ever had this problem before?

Related

How to search in a lazy-loading WPF MVVM TreeView?

I need a TreeView to represent some hierarchical data from multiple tables stored in a SQL Server CE database. Before, the data was stored in xml and was simple deserialized on startup and everything was good. Now I was asked to move data to a database and I've faced a several problems.
My first problem was that it takes quite a long time to retrieve many items from DB and build a TreeView ViewModel from this items (still not sure what is longer - to get items or to construct this tree). So I implemented lazy loading and now I'm getting items only when a TreeViewItem is expanding.
Now, I need to perform a text search over all the nodes, but to make it work, all nodes must be loaded.
I tried to load all of them but the UI freezes while the tree is loading. Doing this inside a BackgroundWorker is also impossible for me because the items are stored in an ObservableCollection and I'm getting "InvalidOperationException". Using Dispatcher helps with this but it is also freezes UI...
The excerpt from my TreeViewItem VM is below, if more code is needed please let me know. Maybe I am totally wrong with my design, so any comments are very appreciated!
public class TreeViewItemViewModel: DisplayableItem, IItemsHost
{
internal static DummyTreeViewItemViewModel _dummy = new DummyTreeViewItemViewModel();
public TreeViewItemViewModel(){}
public TreeViewItemViewModel(IDisplayableItem displayableItem)
{
Data = displayableItem;
}
public TreeViewItemViewModel(IDisplayableItem displayableItem, IDisplayableItem parent)
:this(displayableItem)
{
Parent = parent as TreeViewItemViewModel;
}
private TreeViewItemViewModel _parent;
public TreeViewItemViewModel Parent
{
get { return _parent; }
set { _parent = value; InvokePropertyChanged(new PropertyChangedEventArgs("Parent")); }
}
private IDisplayableItem _data;
public new IDisplayableItem Data
{
get { return _data; }
set { _data = value; InvokePropertyChanged(new PropertyChangedEventArgs("Data")); }
}
private bool _isSelected;
public new bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; InvokePropertyChanged(new PropertyChangedEventArgs("IsSelected")); }
}
private bool _isEnabled=true;
public new bool IsEnabled
{
get { return _isEnabled; }
set { _isEnabled = value; InvokePropertyChanged(new PropertyChangedEventArgs("IsEnabled")); }
}
private bool _isVisible = true;
public new bool IsVisible
{
get { return _isVisible; }
set { _isVisible = value; InvokePropertyChanged(new PropertyChangedEventArgs("IsVisible")); }
}
private void FillItems()
{
if (Items.Contains(_dummy))
{
Items.Remove(_dummy);
var itemshost = Data as IItemsHost;
if (itemshost != null)
{
_items = new ObservableCollection<IDisplayableItem>();
foreach (var item in itemshost.Items)//getting 'Items' actually requesting them from a database
{
var treeItem = new TreeViewItemViewModel(item, this);
_items.Add(treeItem);
}
InvokePropertyChanged(new PropertyChangedEventArgs("Items"));
}
}
}
protected bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if(value)
{
FillItems();
}
_isExpanded = value;
InvokePropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
protected SObservableCollection<IDisplayableItem> _items = new SObservableCollection<IDisplayableItem>();
public SObservableCollection<IDisplayableItem> Items
{
get
{
var itemshost = Data as IItemsHost;
if (itemshost != null)
{
if (_items.Count == 0 && itemshost.Items.Count > 0)
_items.Add(_dummy);
}
return _items;
}
set { _items = value; InvokePropertyChanged(new PropertyChangedEventArgs("Items")); }
}
UPDATE: for those who would search for a similar solution - my problem was in my query method. I shouldn't open a new SQL Server CE connection each time I need to perform a query...
What about a new DB table that holds a flattened representation of the entire hierarchy, and have your search logic query this table? You'll obviously need to keep this table updated as you insert/update/delete records in the other tables.
Each record in the new table would need to include some information about where the item sits in the hierarchy, so that when you get the search results back you can load and populate just those tree nodes containing the "hits".
Since reading from database is being done asynchronously so the performance bottle-neck should be constructing View from ViewModel. I suggest the following method:
Read all essential Model data from database in one async call and store them in an object called SearchHelper.
Add a a simple property (Model.Id or Model's hash code) to every ViewModel that you create in order to find the equivalent view model of an specific model.
Create only visible ViewModels. (lazy loading for ViewModel only)
Use the SearchHelper to find matches for the search query, then using the Id or hash code of the results, you can easily locate their equivalent view models.
Please Consider:
Once loaded, SearchHelper does not update itself, so you might want to manually update it.
For this method to have optimal performance, try avoiding iteration of all nodes. instead, store the sequence of traced items (their index or Id) in order to find them in view model. if each Model item knows its parent, then the back-tracking should be easy.

Binding ComboBox and ObservableCollection<KeyValue> in wpf

in My OpenViewModel i collect data:
private ObservableCollection<KeyValue> availableData;
public ObservableCollection<KeyValue> AvailableDatas
{
get { return availableData; }
set
{
if (value != availableData)
{
availableData= value;
NotifyOfPropertyChange("AvailableDatas");
}
}
}
method for collecting data:
public ObservableCollection<KeyValue> CollectData()
{
ConnectorClient client = null;
try
{
client = webservice.GetClient();
AvailableDatas = client.GetDatas();
client.Close();
}
catch (Exception ex)
{
webservice.HandleException(ex, client);
}
return AvailableDatas;
}
How to call the method CollectData in wpf and fill my COmboBox?
thx
You might simply call the method the first time the AvailableDatas property is accessed (e.g. from a binding in XAML):
private ObservableCollection<KeyValue> availableData;
public ObservableCollection<KeyValue> AvailableDatas
{
get
{
if (availableData == null)
{
availableData = CollectData();
}
return availableData;
}
set
{
if (value != availableData)
{
availableData = value;
NotifyOfPropertyChange("AvailableDatas");
}
}
}
Then you should change the CollectData method in a way that is does not also set the property:
public ObservableCollection<KeyValue> CollectData()
{
ConnectorClient client = null;
ObservableCollection<KeyValue> data = null;
try
{
client = webservice.GetClient();
data = client.GetDatas();
client.Close();
}
catch (Exception ex)
{
webservice.HandleException(ex, client);
}
return data;
}
You could override the OnActivated() event assuming you are using an IScreen implementation and load data in there, or just do it in the constructor or a custom Initialise method if you want to roll your own (or in the property accessor as someone has already said).
You can also use coroutines if you want some visual context for the user and a better tie in with CM actions
There is a nice simple implementation of a Loader class here which helps provide visual context to the user:
https://caliburnmicro.codeplex.com/wikipage?title=IResult%20and%20Coroutines&referringTitle=Documentation
This searches the visual tree for a BusyIndicator control and activates it whilst the content is loading e.g. ...
public class SomeViewModel : Screen
{
protected override void OnActivate()
{
RefreshData();
}
public void RefreshData()
{
Coroutine.BeginExecute(LoadData(), new ActionExecutionContext() { Target = this });
}
public IEnumerable<IResult> LoadData()
{
yield return Loader.Show("Loading Data...");
yield return new LoadSomeDataRoutine(client.GetDatas);
yield return Loader.Hide();
}
}
The reason to have a RefreshData method is that this also allows you to bind CM actions and allows the coroutine can grab more contextual information.
Obviously you have less need to worry about the async->sync benefits this gives in Silverlight because you are using WPF (but it still applies to async web service calls), however it still has many benefits and it also helps you to write reusable routines which become part of your application framework (e.g. some level of error handling/logging encapsulated in the IResult implementation etc)
You also mentioned filling the combobox... all you would need to do in CM is place a combobox on your control, and set it's Name property to the name of the property on your VM:
public class SomeViewModel : Screen
{
public ObservableCollection<MyObject> MyProperty { //blah blah... }
}
<UserControl .... blah>
<ComboBox x:Name="MyProperty" />
</UserControl>
This will fill the combobox with the items. You will still need to set the binding for SelectedItem/SelectedValue
I assume you know this already though - if not CM has some decent documentation:
https://caliburnmicro.codeplex.com/wikipage?title=Basic%20Configuration%2c%20Actions%20and%20Conventions&referringTitle=Documentation

What is the best way to force the WPF DataGrid to add a specific new item?

I have a DataGrid in a WPF application which has for its ItemsSource a custom collection that I wrote. The collection enforces that all its items satisfy a certain requirement (namely they must be between some minimum and maximum values).
The collection's class signature is:
public class CheckedObservableCollection<T> : IList<T>, ICollection<T>, IList, ICollection,
INotifyCollectionChanged
where T : IComparable<T>, IEditableObject, ICloneable, INotifyPropertyChanged
I want to be able to use the DataGrid feature in which committing an edit on the last row in the DataGrid results in a new item being added to the end of the ItemsSource.
Unfortunately the DataGrid simply adds a new item created using the default constructor. So, when adding a new item, DataGrid indirectly (through its ItemCollection which is a sealed class) declares:
ItemsSource.Add(new T())
where T is the type of elements in the CheckedObservableCollection. I would like for the grid to instead add a different T, one that satisfies the constraints imposed on the collection.
My questions are: Is there a built in way to do this? Has somebody done this already? What's the best (easiest, fastest to code; performance is not an issue) way to do this?
Currently I just derived DataGrid to override the OnExecutedBeginEdit function with my own as follows:
public class CheckedDataGrid<T> : DataGrid where T : IEditableObject, IComparable<T>, INotifyPropertyChanged, ICloneable
{
public CheckedDataGrid() : base() { }
private IEditableCollectionView EditableItems {
get { return (IEditableCollectionView)Items; }
}
protected override void OnExecutedBeginEdit(ExecutedRoutedEventArgs e) {
try {
base.OnExecutedBeginEdit(e);
} catch (ArgumentException) {
var source = ItemsSource as CheckedObservableCollection<T>;
source.Add((T)source.MinValue.Clone());
this.Focus();
}
}
}
Where MinValue is the smallest allowable item in the collection.
I do not like this solution. If any of you have advice I would be very appreciative!
Thanks
This problem is now semi-solvable under 4.5 using the AddingNewItem event of the DataGrid. Here is my answer to a similar question.
I solved the problem by using DataGrid's AddingNewItem event. This almost entirely undocumented event not only tells you a new item is being added, but also [allows lets you choose which item is being added][2]. AddingNewItem fires before anything else; the NewItem property of the EventArgs is simply null.
Even if you provide a handler for the event, DataGrid will refuse to allow the user to add
rows if the class doesn't have a default constructor. However, bizarrely (but thankfully) if you do have one, and set the NewItem property of the AddingNewItemEventArgs, it will never be called.
If you choose to do this, you can make use of attributes such as [Obsolete("Error", true)] and [EditorBrowsable(EditorBrowsableState.Never)] in order to make sure no one ever invokes the constructor. You can also have the constructor body throw an exception
Decompiling the control lets us see what's happening in there...
For anybody interested, I ended up solving the problem by just deriving from BindingList<T> instead of ObservableCollection<T>, using my derived class as the ItemsSource in a regular DataGrid:
public class CheckedBindingList<T> : BindingList<T>, INotifyPropertyChanged where T : IEditableObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Predicate<T> _check;
private DefaultProvider<T> _defaultProvider;
public CheckedBindingList(Predicate<T> check, DefaultProvider<T> defaultProvider) {
if (check == null)
throw new ArgumentNullException("check cannot be null");
if (defaultProvider != null && !check(defaultProvider()))
throw new ArgumentException("defaultProvider does not pass the check");
_check = check;
_defaultProvider = defaultProvider;
}
/// <summary>
/// Predicate the check item in the list against.
/// All items in the list must satisfy Check(item) == true
/// </summary>
public Predicate<T> Check {
get { return _check; }
set {
if (value != _check) {
RaiseListChangedEvents = false;
int i = 0;
while (i < Items.Count)
if (!value(Items[i]))
++i;
else
RemoveAt(i);
RaiseListChangedEvents = true;
SetProperty(ref _check, value, "Check");
ResetBindings();
}
}
}
public DefaultProvider<T> DefaultProvider {
get { return _defaultProvider; }
set {
if (!_check(value()))
throw new ArgumentException("value does not pass the check");
}
}
protected override void OnAddingNew(AddingNewEventArgs e) {
if (e.NewObject != null)
if (!_check((T)e.NewObject)) {
if (_defaultProvider != null)
e.NewObject = _defaultProvider();
else
e.NewObject = default(T);
}
base.OnAddingNew(e);
}
protected override void OnListChanged(ListChangedEventArgs e) {
switch (e.ListChangedType) {
case (ListChangedType.ItemAdded):
if (!_check(Items[e.NewIndex])) {
RaiseListChangedEvents = false;
RemoveItem(e.NewIndex);
if (_defaultProvider != null)
InsertItem(e.NewIndex, _defaultProvider());
else
InsertItem(e.NewIndex, default(T));
RaiseListChangedEvents = true;
}
break;
case (ListChangedType.ItemChanged):
if (e.NewIndex >= 0 && e.NewIndex < Items.Count) {
if (!_check(Items[e.NewIndex])) {
Items[e.NewIndex].CancelEdit();
throw new ArgumentException("item did not pass the check");
}
}
break;
default:
break;
}
base.OnListChanged(e);
}
protected void SetProperty<K>(ref K field, K value, string name) {
if (!EqualityComparer<K>.Default.Equals(field, value)) {
field = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
This class is incomplete, but the implementation above is enough for validating lists of statically-typed (not built by reflection or with the DLR) objects or value types.

Suggestions for Entity/Business object validation where the validation is dependent on other Entity/Service

Context
For a WPF application using the MVVM pattern I validate my entity(/business object) using the IDataErrorInfo interface on the entity so that validation rules in my entity are automatically called by WPF and the validationerrors automatically appear in the View. (inspired by Josh Smith in this article: http://joshsmithonwpf.wordpress.com/2008/11/14/using-a-viewmodel-to-provide-meaningful-validation-error-messages/
This works OK for simple validation rules like (name > 10 characters, value must be > 0)
But what to do when the validation rule in the model is more complex (like name must be unique / max value of the property is defined in another entity). I first thought of solving this by let the entity have a reference to a repository, but this doesn't feel good because I think there should only be references from the repository to the entity and not the other way (creating a cyclic reference)
Is it 'legal' to have a reference from the Recipe entity to the ConfigurationRepository. Or do you have a better suggestion?
Do you have suggestions how to implement Entity/Business object validation where the validation is dependent on other Entity/Service, like in the example below.
Below the simplified code of my real world problem.
In the Recipe entity I want to validate that the maximum temperature is less than the value stored in Configuration.MaximumTemperature. How would you solve this?
The Configuration entity (Stores the maximal allowed temperature for a recipe)
public class Configuration: INotifyPropertyChanged, IDataErrorInfo
{
private int _MaxTemperatureSetpoint;
public int MaxTemperatureSetpoint
{
get { return _MaxTemperatureSetpoint; }
set
{
if (value != _MaxTemperatureSetpoint)
{
_Setpoint = value;
RaisePropertyChanged("MaxTemperatureSetpoint");
}
}
}
The Simplified Recipe (Class where the user configures a recipe with a desired temperature (TemperatureSetpoint) and a desired Time (TimeMilliSeconds). The TemperatureSetpoint must be < Configuration.MaxTemperature)
public class Recipe: INotifyPropertyChanged, IDataErrorInfo
{
private int _TemperatureSetpoint;
public int TemperatureSetpoint
{
get { return _TemperatureSetpoint; }
set
{
if (value != _TemperatureSetpoint)
{
_Setpoint = value;
RaisePropertyChanged("Setpoint");
}
}
}
private int _TimeMilliSeconds;
public int TimeMilliSeconds
{
get { return _TimeMilliSeconds; }
set
{
if (value != _TimeMilliSeconds)
{
_TimeMilliSeconds= value;
RaisePropertyChanged("TimeMilliSeconds");
}
}
}
#region IDataErrorInfo Members
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string propertyName]
{
get
{
switch(propertyName)
{
case "TimeMilliSeconds":
//TimeMilliSeconds must be < 30 seconds
if (TimeMilliSeconds < 30000)
{ return "TimeMilliSeconds must be > 0 milliseconds";}
case "TemperatureSetpoint":
//MaxTemperatureSetpoint < maxTemperature stored in the ConfigurationRepository
int maxTemperatureSetpoint = ConfigurationRepository.GetConfiguration().MaxTemperatureSetpoint;
if (TemperatureSetpoint> maxTemperatureSetpoint )
{ return "TemperatureSetpoint must be < " + maxTemperatureSetpoint.ToString();}
}
}
#endregion
}
Recipe Repository
public interface IRecipeRepository
{
/// <summary>
/// Returns the Recipe with the specified key(s) or <code>null</code> when not found
/// </summary>
/// <param name="recipeId"></param>
/// <returns></returns>
TemperatureRecipe Get(int recipeId);
.. Create + Update + Delete methods
}
Configuration Repository
public interface IConfigurationRepository
{
void Configuration GetConfiguration();
}
For validation that is based on business rules, I usually expose a Validation Delegate that my ViewModel can set.
For example, the ViewModel for the Recipe might contain code that looks like this:
public GetRecipe(id)
{
CurrentRecipe = DAL.GetRecipe(id);
CurrentRecipe.AddValidationErrorDelegate(ValidateRecipe);
}
private string ValidateRecipe(string propertyName)
{
if (propertyName == "TemperatureSetpoint")
{
var maxTemp = Configuration.MaxTemperatureSetpoint;
if (CurrentRecipe.TemperatureSetpoint >= maxTemp )
{
return string.Format("Temperature cannot be greater than {0}", maxTemp);
}
}
return null;
}
The idea is that your Model should only contain raw data, therefore it should only validate raw data. This can include validating things like maximum lengths, required fields, and allowed characters. Business Logic, which includes business rules, should be validated in the ViewModel, and this allows that to happen.
The actual implementation of my IDataErrorInfo on the Recipe class would look like this:
#region IDataErrorInfo & Validation Members
/// <summary>
/// List of Property Names that should be validated
/// </summary>
protected List<string> ValidatedProperties = new List<string>();
#region Validation Delegate
public delegate string ValidationErrorDelegate(string propertyName);
private List<ValidationErrorDelegate> _validationDelegates = new List<ValidationErrorDelegate>();
public void AddValidationErrorDelegate(ValidationErrorDelegate func)
{
_validationDelegates.Add(func);
}
#endregion // Validation Delegate
#region IDataErrorInfo for binding errors
string IDataErrorInfo.Error { get { return null; } }
string IDataErrorInfo.this[string propertyName]
{
get { return this.GetValidationError(propertyName); }
}
public string GetValidationError(string propertyName)
{
// If user specified properties to validate, check to see if this one exists in the list
if (ValidatedProperties.IndexOf(propertyName) < 0)
{
//Debug.Fail("Unexpected property being validated on " + this.GetType().ToString() + ": " + propertyName);
return null;
}
string s = null;
// If user specified a Validation method to use, Validate property
if (_validationDelegates.Count > 0)
{
foreach (ValidationErrorDelegate func in _validationDelegates)
{
s = func(propertyName);
if (s != null)
{
return s;
}
}
}
return s;
}
#endregion // IDataErrorInfo for binding errors
#region IsValid Property
public bool IsValid
{
get
{
return (GetValidationError() == null);
}
}
public string GetValidationError()
{
string error = null;
if (ValidatedProperties != null)
{
foreach (string s in ValidatedProperties)
{
error = GetValidationError(s);
if (error != null)
{
return error;
}
}
}
return error;
}
#endregion // IsValid Property
#endregion // IDataErrorInfo & Validation Members
To be honest, I found that the baked in WPF validation methods are not complete and/or elegant enough. I find that using the WPF methods would scatter validation code and logic throughout my application and would even put some in my UI. Like you, I used Custom Business Objects (CBOs) for everything, and I was really wanting to keep my validation in my objects, since I was using them across several projects (a web service, UI, mobile, etc).
What I did was take my CBO (Recipe, in this case), and add some validation methods as properties. Eg:
public Func<string> NameValidation
{
get
{
return () =>
{
string result = null;
if (String.IsNullOrEmpty(Name)) result = "Name cannot be blank";
else if (Name.Length > 100) result = "Name cannot be longer than 100 characters";
return result;
};
}
}
After that, I decorated it with a custom attribute:
[AttributeUsage(AttributeTargets.Property)]
public class CustomValidationMethod : Attribute
{
}
then I created a Validate() method for object-level validation:
public override void Validate()
{
var a = GetType().GetProperties().Where(w => w.GetCustomAttributes(typeof(CustomValidationMethod), true).Length > 0);
foreach (var a2 in a)
{
var result = a2.GetValue(this, null) as Func<string>;
if (result != null)
{
var message = result();
if (message != null)
//There was an error, do something
else if (message == null && Errors.ContainsKey(a2.Name))
//There was no error
}
}
}
then I created custom controls that support my validation. In this case, it was a ComboBox that I derived from the standard ComboBox and added this code:
public Func<string> ValidationMethod
{
get { return (Func<string>) GetValue(ValidationMethodProperty); }
set { SetValue(ValidationMethodProperty, value); }
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (ValidationMethod != null && !String.IsNullOrEmpty(ValidationMethod()))
SetControlAsInvalid();
else
SetControlAsValid();
}
Once this is all set up, I can add field validation in the validation methods (which are stored in my CBOs instead of scattered throughout my code), I can add object-level validation in my Validate() method. As well, I can customize with ease how the control should behave with regards to validation.
To use this, in my VM I would call .Validate() first, then deal with any problems before saving. In my case specifically, I would store error messages in a collection and then query them (this also allowed me to store several error messages instead of the first one)

PropertyChanged notification for calculated properties

I'm developing an application in Silverlight2 and trying to follow the Model-View-ViewModel pattern. I am binding the IsEnabled property on some controls to a boolean property on the ViewModel.
I'm running into problems when those properties are derived from other properties. Let's say I have a Save button that I only want to be enabled when it's possible to save (data has been loaded, and we're currently not busy doing stuff in the database).
So I have a couple of properties like this:
private bool m_DatabaseBusy;
public bool DatabaseBusy
{
get { return m_DatabaseBusy; }
set
{
if (m_DatabaseBusy != value)
{
m_DatabaseBusy = value;
OnPropertyChanged("DatabaseBusy");
}
}
}
private bool m_IsLoaded;
public bool IsLoaded
{
get { return m_IsLoaded; }
set
{
if (m_IsLoaded != value)
{
m_IsLoaded = value;
OnPropertyChanged("IsLoaded");
}
}
}
Now what I want to do is this:
public bool CanSave
{
get { return this.IsLoaded && !this.DatabaseBusy; }
}
But note the lack of property-changed notification.
So the question is: What is a clean way of exposing a single boolean property I can bind to, but is calculated instead of being explicitly set and provides notification so the UI can update correctly?
EDIT: Thanks for the help everyone - I got it going and had a go at making a custom attribute. I'm posting the source here in case anyone's interested. I'm sure it could be done in a cleaner way, so if you see any flaws, add a comment or an answer.
Basically what I did was made an interface that defined a list of key-value pairs to hold what properties depended on other properties:
public interface INotifyDependentPropertyChanged
{
// key,value = parent_property_name, child_property_name, where child depends on parent.
List<KeyValuePair<string, string>> DependentPropertyList{get;}
}
I then made the attribute to go on each property:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
public class NotifyDependsOnAttribute : Attribute
{
public string DependsOn { get; set; }
public NotifyDependsOnAttribute(string dependsOn)
{
this.DependsOn = dependsOn;
}
public static void BuildDependentPropertyList(object obj)
{
if (obj == null)
{
throw new ArgumentNullException("obj");
}
var obj_interface = (obj as INotifyDependentPropertyChanged);
if (obj_interface == null)
{
throw new Exception(string.Format("Type {0} does not implement INotifyDependentPropertyChanged.",obj.GetType().Name));
}
obj_interface.DependentPropertyList.Clear();
// Build the list of dependent properties.
foreach (var property in obj.GetType().GetProperties())
{
// Find all of our attributes (may be multiple).
var attributeArray = (NotifyDependsOnAttribute[])property.GetCustomAttributes(typeof(NotifyDependsOnAttribute), false);
foreach (var attribute in attributeArray)
{
obj_interface.DependentPropertyList.Add(new KeyValuePair<string, string>(attribute.DependsOn, property.Name));
}
}
}
}
The attribute itself only stores a single string. You can define multiple dependencies per property. The guts of the attribute is in the BuildDependentPropertyList static function. You have to call this in the constructor of your class. (Anyone know if there's a way to do this via a class/constructor attribute?) In my case all this is hidden away in a base class, so in the subclasses you just put the attributes on the properties. Then you modify your OnPropertyChanged equivalent to look for any dependencies. Here's my ViewModel base class as an example:
public class ViewModel : INotifyPropertyChanged, INotifyDependentPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
// fire for dependent properties
foreach (var p in this.DependentPropertyList.Where((x) => x.Key.Equals(propertyname)))
{
PropertyChanged(this, new PropertyChangedEventArgs(p.Value));
}
}
}
private List<KeyValuePair<string, string>> m_DependentPropertyList = new List<KeyValuePair<string, string>>();
public List<KeyValuePair<string, string>> DependentPropertyList
{
get { return m_DependentPropertyList; }
}
public ViewModel()
{
NotifyDependsOnAttribute.BuildDependentPropertyList(this);
}
}
Finally, you set the attributes on the affected properties. I like this way because the derived property holds the properties it depends on, rather than the other way around.
[NotifyDependsOn("Session")]
[NotifyDependsOn("DatabaseBusy")]
public bool SaveEnabled
{
get { return !this.Session.IsLocked && !this.DatabaseBusy; }
}
The big caveat here is that it only works when the other properties are members of the current class. In the example above, if this.Session.IsLocked changes, the notification doesnt get through. The way I get around this is to subscribe to this.Session.NotifyPropertyChanged and fire PropertyChanged for "Session". (Yes, this would result in events firing where they didnt need to)
The traditional way to do this is to add an OnPropertyChanged call to each of the properties that might affect your calculated one, like this:
public bool IsLoaded
{
get { return m_IsLoaded; }
set
{
if (m_IsLoaded != value)
{
m_IsLoaded = value;
OnPropertyChanged("IsLoaded");
OnPropertyChanged("CanSave");
}
}
}
This can get a bit messy (if, for example, your calculation in CanSave changes).
One (cleaner? I don't know) way to get around this would be to override OnPropertyChanged and make the call there:
protected override void OnPropertyChanged(string propertyName)
{
base.OnPropertyChanged(propertyName);
if (propertyName == "IsLoaded" /* || propertyName == etc */)
{
base.OnPropertyChanged("CanSave");
}
}
You need to add a notification for the CanSave property change everywhere one of the properties it depends changes:
OnPropertyChanged("DatabaseBusy");
OnPropertyChanged("CanSave");
And
OnPropertyChanged("IsEnabled");
OnPropertyChanged("CanSave");
How about this solution?
private bool _previousCanSave;
private void UpdateCanSave()
{
if (CanSave != _previousCanSave)
{
_previousCanSave = CanSave;
OnPropertyChanged("CanSave");
}
}
Then call UpdateCanSave() in the setters of IsLoaded and DatabaseBusy?
If you cannot modify the setters of IsLoaded and DatabaseBusy because they are in different classes, you could try calling UpdateCanSave() in the PropertyChanged event handler for the object defining IsLoaded and DatabaseBusy.

Resources