Using IDataErrorInfo with Nested Objects
Asked Answered
G

3

6

I am using MVVM and I want to use IDataErrorInfo to validate my View.

My current implementation includes Nested objects and different ViewModels. e.g. Business Entity 'Customer' contains Business Entity 'Address'. I am accessing Address directly in my view, like "Customer.Address". To validate changes in Address I would have to implement IDataErrorInfo in Address.

I use Customer or Address in different Views/ViewModels. Usage in different Views/ViewModels lead to different Validation Behavior. Thus, implementing the validation in the Entity itself is insufficient.

Exposing the properties I want to change directly in the ViewModel (creating new Properties that directly set/get the entity) seems to make the ViewModel way too rigid. and quite too large.

I cannot inherit from Base Classes, as some Business Entities already derive from other objects (A fact I cannot change). The only option I see at the moment is adding an interface to the ViewModel to the Business Entities, and forwarding this[] calls in the Business Entities to that ViewModel Interface.

Is there a best practice on how to validate these nested objects in the ViewModel?

EDIT: One more reason Validation I don't see Validation in the Business Objects as a usable idea is that I need different Business Objects in my ViewModel to validate the View and the data entry.

Genuflect answered 17/9, 2012 at 11:32 Comment(0)
B
8

One way I have done this in the past is to expose a ValidationDelegate on the Model, which allows the ViewModel to attach its own validation code to the model.

Typically I do this because I use the Model layer as plain data objects, so my Models only validate basic things such as max-length or not-nulls, while any advanced validation not specific to the data model gets done in the ViewModel. This typically includes things such as ensuring an item is unique, or that a user has permission to set a value to a specific range, or even something like your case where the validation only exists for a specific action.

public class CustomerViewModel
{
    // Keeping these generic to reduce code here, but it
    // should include PropertyChange notification
    public AddressModel Address { get; set; }

    public CustomerViewModel()
    {
        Address = new AddressModel();
        Address.AddValidationDelegate(ValidateAddress);
    }

    // Validation Delegate to validate Adderess
    private string ValidateAddress(object sender, string propertyName)
    {
        // Do your ViewModel-specific validation here.
        // sender is your AddressModel and propertyName 
        // is the property on the address getting validated

        // For example:
        if (propertyName == "Street1" && string.IsNullOrEmpty(Address.Street1))
            return "Street1 cannot be empty";

        return null;
    }
}

Here's the code I usually use for the validation delegate:

#region IDataErrorInfo & Validation Members

#region Validation Delegate

public delegate string ValidationDelegate(
    object sender, string propertyName);

private List<ValidationDelegate> _validationDelegates = 
    new List<ValidationDelegate>();

public void AddValidationDelegate(ValidationDelegate func)
{
    _validationDelegates.Add(func);
}

public void RemoveValidationDelegate(ValidationDelegate func)
{
    if (_validationDelegates.Contains(func))
        _validationDelegates.Remove(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)
{
    string s = null;

    foreach (var func in _validationDelegates)
    {
        s = func(this, propertyName);
        if (s != null)
            return s;
    }

    return s;
}

#endregion // IDataErrorInfo for binding errors

#endregion // IDataErrorInfo & Validation Members
Backsheesh answered 17/9, 2012 at 13:56 Comment(5)
So "IDataErrorInfo & Validation Members" needs to be implemented in every Business Entity, if I understood that right? As I can't inherit from any Base object, this would have to be pasted in every entity.Genuflect
@Genuflect I use this code in my base Model class all the time, and attach the validation in the Model's constructor with AddValidationDelegate(func). I suppose you could also copy/paste it to every class if you wanted and add your validation in the GetValidationError(propertyName) method, but I think it's far better to implement this in the base class.Backsheesh
A base class would be nice, yes, but due to some restrictions I cannot guarantee that I am able to use a base class in my models.Genuflect
@Genuflect I just re-read your question and realized you said you can't inherit from a base class in many cases, so yes you'd have to copy/paste this code in your Models and be sure they implement IDataErrorInfo. I suppose another way depending on your existing base classes would be to extend the base class first to add validation, then base your models off the extended base class instead of the base class directly. For example, CustomerModel may inherit from ValidatingCustomerBase, and ValidatingCustomerBase inherits from CustomerBaseBacksheesh
@Backsheesh Is this possible to do with INotifyDataErrorInfo, if so can you point me in the right direction for more information, many thanks.Hooey
P
1

Usage in different Views/ViewModels lead to different Validation Behavior.

Hence, you have different view models. If you can't inherit these view models from some base view model, use aggregation:

public class Address {}

public class AddressViewModel1 : IDataErrorInfo
{
  private readonly Address address;
  // other stuff here
}

public class AddressViewModel2 : IDataErrorInfo
{
  private readonly Address address;
  // other stuff here
}
Planet answered 17/9, 2012 at 11:40 Comment(7)
That's not a valid solution for me. This makes the problem/ViewModel even more rigid than exposing the needed Properties in the ViewModel itself. To clarify: There are Business Objects where we (potentially) would have to add hundreds of these aggregated classes, that's just something I won't do. Also, using a different class in every ViewModel is not something I want to (and can) do either.Genuflect
@Andreas: why this is rigid? You still can use VM from the view like Customer.Address, but you can write different validation behavior.Planet
@Andreas: you have BO, which can be validated using 100 or more different validation rule sets? Could you give an example of that object?Planet
Well, I would have to make a different AddressViewModel class for each ViewModel, that's quite confusing in the long run. Also, I have to add that to validate most objects, I need other objects in the ViewModelGenuflect
OK, hundreds might be a bit exaggerated, as a lot of cases can be displayed through parameters. It doesn't change the fact I stated in my previous comments (and updated in the original post) however.Genuflect
@Andreas: If you want to write some kind of universal view model, which can contain any type of business object, then it's a wrong way to go. This is completely unsupportable solution.Planet
@Dennsi: Not universal per se, but more on a per-View basis. In many cases Validation is spanning multiple Business Entities, even if it's only one View or ViewModel. Splitting the down the ViewModels even more wouldn't help me, as the Business Entities Spanning still exists.Genuflect
G
0

what about using dependency injection and inject a validationservice to the customer object for each different viewmodel?

but i think implementing idataerrorinfo and all needed properties in your viewmodel would be cleaner, but of course one time more work.

Gotcher answered 17/9, 2012 at 13:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.