문제

Is there a best practice or widely accepted way of structuring and validating data using MVVM in conjunction with RIA services in Silverlight?

Here's the crux of my problem. Let's say I have an EmployeeView, EmployeeViewModel and some Employee entity. In regular RIA applications I will expose that Employee entity on the view and I get validation "for free", because Entities implement INotifyDataErrorInfo and IDataErrorInfo (correct?).

Now if I want to expose some Employee properties through a ViewModel instead of directly through an Entity then it becomes more complicated. I could expose the bits that I need directly and hook them into the entity on the backend, like this:

    private Employee _employee;

    public EmployeeViewModel()
    {
        _employee = new Employee();
    }

    public string Name
    {
        get { return _employee.Name; }
        set
        {
            _employee.Name = value;
            // fire property change, etc.
        }
    }

... but I lose the tasty "free" validation of entities. Otherwise, I could expose the entity directly in the view model, like so

    private Employee _employee;
    public Employee Employee
    {
        get { return _employee; }
    }

    public EmployeeViewModel()
    {
        _employee = new Employee();
    }

In this case, the view will bind directly to the Employee entity and find its properties in there, like so:

<StackPanel DataContext="{Binding Employee}">
    <TextBox Text="{Binding Name}" />
</StackPanel>

Using this method we get "free" validation, but it's not exactly a clean implementation of MVVM.

A third option would be to implement INotifyDataErrorInfo and IDataErrorInfo myself in the VMs, but this seems like an awful lot of plumbing code, considering how easy it would be for me to use the above solution and have something slightly less "clean" but a heck of a lot easier at the end of the day.

So I guess my question is, which of these approaches are appropriate in which situation? Is there a better approach I am missing?

In case it's relevant I'm looking at the Caliburn.Micro MVVM framework, but I would be keen to see answers that apply generically.

도움이 되었습니까?

해결책

I am using RIA with Caliburn.Micro and am pretty happy with my solution for client side validation.

What I have done is put a ValidationBaseViewModel between Screen (provided by Caliburn.Micro) and my actual application VMs (EmployeeViewModel in your case). ValidationBaseViewModel implements INotifyDataErrorInfo so that plumbing code your talking about is only written once. I then add/remove/notify of errors via ValidationBaseViewModel from an override of the (Caliburn.Micro) PropertyChangedBase.NotifyOfPropertyChange with the following code:

public override void NotifyOfPropertyChange(string property)
{
    if (_editing == null)
        return;

    if (HasErrors)
        RemoveErrorFromPropertyAndNotifyErrorChanges(property, 100);

    if (_editing.HasValidationErrors)
    {
        foreach (var validationError in
                       _editing.ValidationErrors
                               .Where(error => error.MemberNames.Contains(property)))
        {
            AddErrorToPropertyAndNotifyErrorChanges(property, new ValidationErrorInfo() { ErrorCode = 100, ErrorMessage = validationError.ErrorMessage });
        }
    }

    base.NotifyOfPropertyChange(property);
}

This is actually in another VM (between ValidationBaseViewModel and EmployeeViewModel) with the following definition:

public abstract class BaseEditViewModel<TEdit> :
                                ValidationBaseViewModel where TEdit : Entity

where Entity is RIAs System.ServiceModel.DomainServices.Client.Entity and the _editing class member is an instance of this type TEdit which is being edited by the current VM.

In combination with Caliburn coroutines this allows me to do some cool stuff like the following:

[Rescue]
public IEnumerable<IResult> Save()
{
    if (HasErrors)
    {
        yield return new GiveFocusByName(PropertyInError);
        yield break;
    }

    ...
}

다른 팁

If you don't want to use external resources or frameworks, then I you could have a ViewModelBase that implement INotifyDataErrorInfo.

That class will have ValidateProperty(string propertyName, object value) to validate a speciic property, and Validate() method to validate the entire object. Internally use the Validator class to return the ValidationResults.
If you use reflector, it can be pretty easy to achieve by mimicking the validation process in the Entity class itself to the ViewModelBase.

Although it's no "free", is still relatively cheap tho.

Here is a sample implementation of IDataErrorInfo. Although not tested, will give you the idea.

public class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{

  /*
   * InotifyPropertyChanged implementation
   * Consider using Linq expressions instead of string names
   */

  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  public IEnumerable GetErrors(string propertyName)
  {
    if (implValidationErrors == null) return null;
    return ImplValidationErros.Where(ve =>
      ve.MemberNames.Any(mn => mn == propertyName));
  }

  public bool HasErrors
  {
    get
    {
      return implValidationErrors == null || ImplValidationErros.Any();
    }
  }

  private List<ValidationResult> implValidationErrors;
  private List<ValidationResult> ImplValidationErros
  {
    get
    {
      return implValidationErrors ?? 
        (implValidationErrors = new List<ValidationResult>());
    }
  }
  private ReadOnlyCollection<ValidationResult> validationErrors;
  [Display(AutoGenerateField = false)]
  protected ICollection<ValidationResult> ValidationErrors
  {
    get
    {
      return validationErrors ?? 
        (validationErrors =
        new ReadOnlyCollection<ValidationResult>(ImplValidationErros));
    }
  }
  protected void ValidateProperty(string propertyName, object value)
  {
    ValidationContext validationContext =
      new ValidationContext(this, null, null);
    validationContext.MemberName = propertyName;
    List<ValidationResult> validationResults =
      new List<ValidationResult>();

    Validator.TryValidateProperty(
      value, 
      validationContext, 
      validationResults);

    if (!validationResults.Any()) return;

    validationResults
      .AddRange(ValidationErrors
      .Where(ve =>
        !ve.MemberNames.All(mn =>
          mn == propertyName)));

    implValidationErrors = validationResults;

    if (ErrorsChanged != null)
      ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
  }
}

you can use a partial class to extend your entitty and add data validation there via idataerrorinfo.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top