Manish Dalal's blog

Exploring .net!

Silverlight Business Application Part 6: IEditableobject and Add new item (Take 3!)

This is part six of Building Business Application with Silverlight series that showcases the basic building blocks of a data centric application.  

Series Link: Part 0, Part 1, Part 2, Part 3, Part 4, Part 5

We have seen how to add new items, delete items and how to validate data using various mechanisms. Now we will tackle some of the essentials features.

IEditableObject

When a validation error occurs in a cell, user can back out of changes using Escape key. However if user has made changes to other columns in same row, those changes will still persists. User may wish to back out of all changes. This is where IEditableObject interface comes into play. IEditableObject is used to represent an object that has an editing mode and the ability to commit or revert changes. By implementing this interface, when user Escapes first time, cell value is reverted, when user Escapes again (second time), entire row is reverted back.

Add IEditableObject to the Person class declaration in the Silverlight project.

public class Person : INotifyPropertyChanged, IEditableObject {

Add following code just before end of the class to implement IEditableObject interface.

 
#region IEditableObject Members
Person _backup;
bool _editing;

public void BeginEdit() {
    if (!_editing) {
        _editing = true;
        _backup = this.MemberwiseClone() as Person;
    }
}

public void CancelEdit() {
    if (_editing) {
        FirstName = _backup.FirstName;
        LastName = _backup.LastName;
        Age = _backup.Age;
        City = _backup.City;
        _editing = false;
    }
}


public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
    }
}
#endregion

Here we are making copy of current data to the field backup in BeginEdit. If user decides to cancel, we just restore original data from backup, otherwise we just remove the backup. Please note that MemberwiseClone method only creates a shallow copy and it is ok for the demo, but for production code please create a deep copy.

F5 and run the application. Make change to the age filed. Next make change to Last name and First Name.

image

Now undo change by pressing Esc. On first Escape, First name is reverted back.

image

Esc again. On second Escape, Age and Last name is reverted back.

image

Delay or Not to Delay - That is the question!

In our example, we have kept property change notification live, in real time. Anytime there is a change to data, ProeprtyChanged event is fired and other parts of UI are updated. This is prefect for GridView / DetailsView kind of scenario, but may create issue if you are performing complicated calculation or other changes in response to change to property. One option is to delay property change notification till end. Only when user commits, data is compared to backup copy and property change notification sent out. If user cancel, no notification is sent and rest of application is not affected by rollback to original values.

Now that we have implemented IEditableObject, we know exactly when user has committed changes. We can use this information to enhance Add New Item functionality.

Add New Item - Take 3

If you recall from Part 1, when we implemented Add Item, we hooked on to PropertyChanged event notification to trigger adding of new row. It worked, except when user backed out of change by deleting data, code still added new row. This might be ok, but we can remedy this undesirable behavior by utilizing out now found ability to track user commits!

Add new event ChangesCommited to Person class as follows

public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
        if (null != ChangesCommitted) {
            OnChangesCommitted();
        }
    }
}

bool _fireChangesCommited;
public event EventHandler<EventArgs> ChangesCommitted;

protected void OnChangesCommitted() {
    if (null != ChangesCommitted) {
        ChangesCommitted(this, new EventArgs());
    }
}

We have defined new ChangesCommitted event that will fire when user finishes editing and commits changes, through call to EndEdit (by DataGrid). If use rollbacks changes, no Commit event is fired and we will not add unnecessary empty row.

Next change People class to subscribe to ChangesCommitted in place of PropertyChanged. Also add emptyPerson_ChangesCommitted event handler. Comment out old emptyPerson_PropertyChanged event handler.
 
public People() {
    emptyPerson = new Person();
    //emptyPerson.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(emptyPerson_PropertyChanged);
    emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    base.InsertItem(this.Count, emptyPerson);
}

void emptyPerson_ChangesCommitted(object sender, EventArgs e) {
    emptyPerson.ChangesCommitted -= new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    emptyPerson = new Person();
    emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    base.InsertItem(this.Count, emptyPerson);

}

When we receive ChangesCommitted on emptyPerson, we know that user has committed changes and we add new emptyPerson, resulting in new empty row being added to DataGrid.

F5 and test the application. Change First Name, notice new row is not added. Change Last name. Now undo all name changes. No new row will be added.

We still have a big issue to take care of. Validation! Try this, just enter First Name, and navigate away from row. DataGrid will commit changes and new row will added. This is not what we want, we need to make sure that Person is valid before committing changes and adding new row. Lets add that crucial validation. What we need to do is to validate data in EndEdit and if data is valid, fire ChangesCommitted event. PersonValidator already has method "Validate" to validate person out of band and we will use that.

Now if we had just to deal with sync validation only, this would turn out to be a simple call, if result of validation is true, we will fire event. However, we are validating city in async manner and that complicates things quite a bit. We need to delay firing of committed event till we get results back from async city validation. Lets see how to do this.

Modify EndEdit as follows

public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
        if (null != ChangesCommitted) {
            _fireChangesCommited = true;
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => _validator.Validate());
        }
    }
}
 
Note that we are first validating data and postponing firing ChangesCommitted. Once data is validated, PersonValidator needs to let Person class notify of availability of validation result. This is required since we are carrying out City validation in async fashion. If you do not have any async validation, you can just validate inline and if valid, fire event immediately.
 
Replace PersonValidator with following code:
using Silverlight.CityServiceReference;
using System.ComponentModel;
using System.Collections.Generic;
using System.Windows.Threading;

namespace Silverlight {
    public class PersonValidator : INotifyPropertyChanged {

        #region field/properties
        public const string PROPERTY_NAME_INVALID = "Invalid";
        private Person _data;
        private Dictionary<string,string> _errors;
        private List<string> _propertiesToValidate;
        private bool _fireValidated;

        #region Age
        public const string PROPERTY_NAME_AGE = "Age";

        public bool InvalidAge {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_AGE);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_AGE, "Age must be greater than 0 and less than 200");
                } else {
                    ClearError(PROPERTY_NAME_AGE);
                }
            }
        }

        public void ValidateAge(int newValue) {
            InvalidAge = (newValue < 0 || newValue > 200);
        }
        #endregion

        #region LastName
        public const string PROPERTY_NAME_LASTNAME = "LastName";

        public bool InvalidLastName {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_LASTNAME);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_LASTNAME, "Last Name is required");
                } else {
                    ClearError(PROPERTY_NAME_LASTNAME);
                }
            }
        }

        public void ValidateLastName(string newValue) {
            if(string.IsNullOrEmpty(newValue)){
                InvalidLastName = true;
                return;
            }
            InvalidLastName = (0 == newValue.Trim().Length);
        }
        #endregion

        #region City
        public const string PROPERTY_NAME_CITY = "City";

        public bool InvalidCity {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_CITY);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_CITY, "City is not valid.");
                } else {
                    ClearError(PROPERTY_NAME_CITY);
                }
            }
        }

        private void ValidateCity(string city) {
            if (string.IsNullOrEmpty(city)) {
                InvalidCity = true;
                return;
            }
            CityServiceClient proxy = new CityServiceClient();
            proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
            ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");
            proxy.IsCityValidAsync(city, city);
        }

        void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
            ((Page)Application.Current.RootVisual).EndWait(null);
            InvalidCity = !e.Result;
        }
        #endregion

        #region IsValid
        public const string PROPERTY_NAME_ISVALID = "IsValid";

        public bool IsValid {
            get { 
                return (0 == _errors.Keys.Count);
            }
            set {
                OnPropertyChanged(PROPERTY_NAME_ISVALID);
                Application.Current.RootVisual.Dispatcher.BeginInvoke(() => _data.RaisePropertyChanged("Validator"));
            }
        }
        #endregion

        private bool _manualValidation;
        // use to turn off automatic property validation
        // used with IEditableObject 
        public bool ManualValidation {
            get { return _manualValidation; }
            set { _manualValidation = value; }
        }

        #endregion

        #region Constructors
        public PersonValidator(Person data) {
            _data = data;
            _data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Person_PropertyChanged);
            // default valid
            _errors = new Dictionary<string, string>();
            
        }

        public PersonValidator(Person data, bool defaultInvalid)
            : this(data) {
            if (defaultInvalid) {
                _errors.Add(PROPERTY_NAME_AGE, PROPERTY_NAME_AGE);
                _errors.Add(PROPERTY_NAME_CITY, PROPERTY_NAME_CITY);
                _errors.Add(PROPERTY_NAME_LASTNAME, PROPERTY_NAME_LASTNAME);
            }
        }
        #endregion

        #region Validation Management
        // automatic validation
        void Person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
            if (e.PropertyName == "IsValid") {
                return;
            }
            if (ManualValidation) {
                // can be used to cache list of properties that need to be validated
                return;
            }
            switch (e.PropertyName) {
                case PROPERTY_NAME_AGE:
                    ValidateAge(_data.Age);
                    break;
                case PROPERTY_NAME_LASTNAME:
                    ValidateLastName(_data.LastName);
                    break;
                case PROPERTY_NAME_CITY:
                    ValidateCity(_data.City);
                    break;

            }
        }
        
        // manual, on demand validation
        public void Validate() {
            _fireValidated = true;
            _propertiesToValidate = new List<string>() { 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_AGE, 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_CITY, 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_LASTNAME };
            //
            ValidateAge(_data.Age);
            ValidateLastName(_data.LastName);
            ValidateCity(_data.City);
        }

        public bool AllPropertiesValidated(string propertyValidated) {
            _propertiesToValidate.Remove(propertyValidated);
            return (_propertiesToValidate.Count == 0);
        }

        public event EventHandler<EventArgs> Validated;

        protected void OnValidated() {
            if (null != Validated) {
                Validated(this, new EventArgs());
            }
        }
        #endregion

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string name) {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            //
            if (_fireValidated && AllPropertiesValidated(name)) { 
                _fireValidated = false;
                Application.Current.RootVisual.Dispatcher.BeginInvoke(() => OnValidated());
            }
        }
        #endregion

        #region Error/State management
        public string this[string propertyName] {
            get {
                if (_errors.ContainsKey(propertyName)) {
                    return _errors[propertyName];
                } else {
                    return null;// propertyName;
                }
            }
        }

        public void RegisterError(string propertyName, string message) {
            if (_errors.ContainsKey(propertyName)) {
                _errors[propertyName] = message;
            } else {
                _errors.Add(propertyName, message);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => IsValid = false);
        }

        public void ClearError(string propertyName) {
            if (_errors.ContainsKey(propertyName)) {
                _errors.Remove(propertyName);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => IsValid = true);
        }
        #endregion
    }
}
 
We have added Validated event and OnValidated method to fire event. We also midfield Validate method to turn on delay firing of validated event by setting fireValidated to true. Next OnProeprtyChanged is modified to check if all properties are validated and if we have pending validated event to fire, then we trigger validated event. Note the use of Dispatcher to fire events asynchronously.
 
Now we need to change Person class to subscribe to Validated event.
 
public Person() {
    _validator = new PersonValidator(this);
    _validator.Validated += new EventHandler<EventArgs>(_validator_Validated);
}

void _validator_Validated(object sender, EventArgs e) {
    if (_fireChangesCommited) {
        _fireChangesCommited = false;
        if (_validator.IsValid) {
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => OnChangesCommitted());
        }
    }
}
 
In constructor, when we are setting up validator, we are also subscribing to validated event. In event handler, we fire ChangesCommitted if data is valid.
 
Finally, F5 and test the application. Add First name, last name and navigate away from row.
 
image
 
No new row is added, and City field has turned red to indicate invalid city.
 
Now enter age and valid city. When you navigate away from row, new row is added
 
image
 
You may have noticed that in order to add new row, you have to navigate up from the last row. This is not normal user behavior. User will try to navigate down, not up! Lets fix that. Modify Page.xaml.cs as follows
 
public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    //this.addButton.Click += new RoutedEventHandler(addButton_Click);
    this.peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
    this.deleteButton.Click += new RoutedEventHandler(deleteButton_Click);
    this.peopleDataGrid.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(peopleDataGrid_BindingValidationError);
    this.peopleDataGrid.KeyUp += new KeyEventHandler(peopleDataGrid_KeyUp);
}

void peopleDataGrid_KeyUp(object sender, KeyEventArgs e) {
    if (Key.Down == e.Key || Key.Enter == e.Key) {
        peopleDataGrid.EndEdit(true, true);
    }
}
 
When user presses Down or Enter key, we force DataGrid to EndEdit, which in turn will trigger validation and if everything checks out ok, eventually resulting in addition of new person/row.
 
We now have a complete application that allows to add new "validated" item, delete item and shows how to carry out extensive validation. We have tackled both sync and async validation. Hopefully you have gained enough know how to start building you own LOB application.
 
While validation is required, it would be better if we can restrict user input and prevent erroneous entry in the first place. We will see how to achieve that next time!
 

Comments

Ken Cox [MVP] said:

Hi Manish,

When you're finished this series, will you be making the complete project source code available?

Ken

# September 17, 2008 9:50 PM

manish.dalal said:

Hi Ken,

I will post complete source code in the next post.

# September 18, 2008 1:24 PM

... said:

Gute Arbeit hier! Gute Inhalte.

# March 3, 2009 5:56 AM

... said:

Sehr wertvolle Informationen! Empfehlen!

# March 12, 2009 6:14 PM

Braulio said:

Interesting, one drawback of IEditable comes when you want to show a message to the user asking whether canceling the edit or not, unfortunately all model messages (artifical stuff) that you can show on SL does not stop the execution flow...

# May 1, 2009 7:45 PM

Boy42 said:

Imagine that nobody harbors   racial animus or prejudice. ,

# October 22, 2009 8:30 AM