The management and validation of data is a common use case in nearly all applications which contains a graphical user interface. Such applications often have dialogs to allow a data input. In a modern user interface the user should have a use friendly data validation and the possibility to see and undo data changes.
Of course, you as developer should and want not reinvent the wheel in every application. Therefore I want to offer you a reusable solution within this and the following articles. I will split up the subject into three articles. Within this first article I want to introduce a data class which can manage a single value. Based on this data class we will create a data collection to manage complex data structures. And in the third article we will use this data collection within a WPF user interface.
Requirements for the Property class
At first we have to think about the property class. This class should store a single value. This might be a value of the base types like string, int, double, etc. or even a complex data class. So the Property class should be implemented to store a generic data type.
A basic requirement for our data class would be the data management with the possibility to detect changes and undo changes. I want to keep this function as simple as possible. Therefore I don’t want to implement a multistage undo and redo feature because this is already implemented in the user controls in WPF. But the data class should offer a single undo feature. Such functionality may be used to detect changes and reset the data to a start value, e.g. the value of the last commit.
Another important feature is the data validation. So the Property class should have functions to check whether the value is valid or not and to get detailed information about validation errors.
I also thought about change notifications and decided to leave them off. That’s because I want to create a data class which can be used universal. So you can use it in the data layer, business layer and user interface layer of your application and implement you own notification methods if needed. Furthermore I always want to have “stupid” data classes. I strongly distinguish between data management and application behavior. So my typical data classes will only provide things like data management, cloning and data validation. Other features like data observation, store, load and convert data or other similar functions should be done by service classes, independent from the data classes.
Property class Interface
As we have broken down the requirements to a basic set of features, we can define the interface of the Property class. The following source code shows the according interface implementation.
using System.Collections.ObjectModel; namespace DataDomain { public interface IProperty { bool IsChanged { get; } void RevertChanges(); void AcceptChanges(); bool IsValid { get; } ObservableCollection<string> ValidationErrors { get; } } }
The functions IsChanged, RevertChanges and AcceptChanges are used to see and undo changes. For the data validation we will have the properties IsValid and ValidationErrors.
Member
At first we have to implement the generic data class. It should store a value and it should allow an undo of changes. So the data class will get two members to store the origin and actual value. Furthermore we want to have validation features. I don’t want to do a validation on each access to the according validation properties. As long as the value stays the same, also the validation result is the same. So we create two more members, to store the validation results and another to know whether the validation was already done.
Another important issue is the validation and comparison of the data itself. How can we implement these features for generic values? For the equality check we can use the default comparer of the basic data types but this will not work for reference types because in this case we want to compare the content and not the pointer itself. Or think about a simple string comparison: do we want to check or ignore case differences? So as we use the Property class it must be possible to define the validation and comparison functionality for each class instance individually. Therefore we will allow defining according functions. So we have to create two more members to store these optional functions. The following source code shows the according generic data class with all needed member variables.
using System; using System.Collections.ObjectModel; namespace DataDomain { public class Property<T> : IProperty { #region Member private T _value; private T _originValue; private bool _validationChecked; private ObservableCollection<string> _validationErrors; private Func<T, T, bool> _equalityChecker; private Func<T, ObservableCollection<string>> _validator; #endregion } }
ctor
As we want to offer the optional validation and comparison we have may overload the constructor. So the user can set the according functions or not. The following source code shows the needed constructors.
using System; using System.Collections.ObjectModel; namespace DataDomain { public class Property<T> : IProperty { #region ctor /// <summary> /// standard constructor /// </summary> public Property() { _value = default(T); _originValue = default(T); } /// <summary> /// constructor with addidional equality checker /// </summary> public Property(Func<T, T, bool> equalityChecker) : this() { _equalityChecker = equalityChecker; } /// <summary> /// constructor with addidional validation checker /// </summary> public Property(Func<T, ObservableCollection<string>> validator) : this() { _validator = validator; } /// <summary> /// constructor with addidional validator and equality checker /// </summary> public Property( Func<T, ObservableCollection<string>> validator, Func<T, T, bool> equalityChecker) : this() { _validator = validator; _equalityChecker = equalityChecker; } #endregion } }
If you want to create a Property instance with an own validator or equality check you may use anonymous functions. The following example shows how to create a string property with an own equality checker.
Func<string, string, bool> equalityChecker = delegate(string a, string b) { return !a.Equals(b,StringComparison.OrdinalIgnoreCase); }; Property<string> data = new Property<string>(equalityChecker);
Data management
At next we want to implement the data management. So we need a property to set and get the actual value, we want functions to accept or reset changes and we need a property to check whether the value was changed. The following source code shows a possible implementation. The IsChecked implementation is the most difficult one because we can use the optional equality checker or the default one. If we use the default one we also have to check for null values.
using System; using System.Collections.ObjectModel; namespace DataDomain { public class Property<T> : IProperty { #region Value management /// <summary> /// get or set the actual value /// </summary> public T Value { get { return _value; } set { _value = value; _validationChecked = false; } } /// <summary> /// get whether the value was changed /// </summary> public bool IsChanged { get { if (_equalityChecker == null) { if((_value == null) && (_originValue == null)) { return false; } else if((_value == null) || (_originValue == null)) { return true; } else { return !_value.Equals(_originValue); } } else { return _equalityChecker(_originValue, _value); } } } /// <summary> /// reverts all changes to the last value, set with 'AcceptChanges' /// </summary> public void RevertChanges() { Value = _originValue; } /// <summary> /// accepts all changes, this will set the new value for a revert /// </summary> public void AcceptChanges() { _originValue = _value; } #endregion } }
Validation
As described above the validation is based on a validation function. Furthermore it will only be executed once and the results will be cached till the property value is changed. The source code shows an according implementation.
using System; using System.Collections.ObjectModel; namespace DataDomain { public class Property<T> : IProperty { #region Validation /// <summary> /// get whether the value is valid /// </summary> public bool IsValid { get { if (_validationChecked == false) { CheckValidation(); } if (_validationErrors != null) { if (_validationErrors.Count > 0) { return false; } } return true; } } /// <summary> /// get validation errors /// </summary> public ObservableCollection<string> ValidationErrors { get { if (_validationChecked == false) { CheckValidation(); } return _validationErrors; } } /// <summary> /// check validation /// </summary> private void CheckValidation() { _validationChecked = true; //execute if (_validator != null) { _validationErrors = _validator(_value); } //set list to null if it is empty if (_validationErrors != null) { if (_validationErrors.Count == 0) { _validationErrors = null; } } } #endregion } }
The following example shows how to define a string value with an own validator function. In this case the string must start with ‘A’ and must at least have a length of three elements.
Func<string, ObservableCollection<string>> validator = delegate(string value) { ObservableCollection<string> errors = new ObservableCollection<string>(); if (value == null) { errors.Add("Value is null"); } else { if (value.Length < 3) { errors.Add("Value must contain at least three elements"); } if (value.StartsWith("A") == false) { errors.Add("Value must start with 'A'"); } } return errors; }; Property<string> data = new Property<string>(validator);
Property class
With the implementation of the validation functionality the Property class is finished. The following code shows the full class. You may use it in your projects and of course feel free to adapt it to your own needs.
using System; using System.Collections.ObjectModel; namespace DataDomain { public class Property<T> : IProperty { #region ctor /// <summary> /// standard constructor /// </summary> public Property() { _value = default(T); _originValue = default(T); } /// <summary> /// constructor with addidional equality checker /// </summary> public Property(Func<T, T, bool> equalityChecker) : this() { _equalityChecker = equalityChecker; } /// <summary> /// constructor with addidional validation checker /// </summary> public Property(Func<T, ObservableCollection<string>> validator) : this() { _validator = validator; } /// <summary> /// constructor with addidional validator and equality checker /// </summary> public Property( Func<T, ObservableCollection<string>> validator, Func<T, T, bool> equalityChecker) : this() { _validator = validator; _equalityChecker = equalityChecker; } #endregion #region Value management /// <summary> /// get or set the actual value /// </summary> public T Value { get { return _value; } set { _value = value; _validationChecked = false; } } /// <summary> /// get whether the value was changed /// </summary> public bool IsChanged { get { if (_equalityChecker == null) { if((_value == null) && (_originValue == null)) { return false; } else if((_value == null) || (_originValue == null)) { return true; } else { return !_value.Equals(_originValue); } } else { return _equalityChecker(_originValue, _value); } } } /// <summary> /// reverts all changes to the last value, set with 'AcceptChanges' /// </summary> public void RevertChanges() { Value = _originValue; } /// <summary> /// accepts all changes, this will set the new value for a revert /// </summary> public void AcceptChanges() { _originValue = _value; } #endregion #region Validation /// <summary> /// get whether the value is valid /// </summary> public bool IsValid { get { if (_validationChecked == false) { CheckValidation(); } if (_validationErrors != null) { if (_validationErrors.Count > 0) { return false; } } return true; } } /// <summary> /// get validation errors /// </summary> public ObservableCollection<string> ValidationErrors { get { if (_validationChecked == false) { CheckValidation(); } return _validationErrors; } } /// <summary> /// check validation /// </summary> private void CheckValidation() { _validationChecked = true; //execute if (_validator != null) { _validationErrors = _validator(_value); } //set list to null if it is empty if (_validationErrors != null) { if (_validationErrors.Count == 0) { _validationErrors = null; } } } #endregion #region Member private T _value; private T _originValue; private bool _validationChecked; private ObservableCollection<string> _validationErrors; private Func<T, T, bool> _equalityChecker; private Func<T, ObservableCollection<string>> _validator; #endregion } }
Pingback: Management and Validation of Data: the PropertyCollection class | coders corner
Pingback: Management and Validation of Data: the User Interface | coders corner