使用 WCF RIA 和 MVVM 模式验证 UI 上的数据

发布于 2024-10-03 09:04:53 字数 1398 浏览 0 评论 0原文

是否存在使用 MVVM 与 Silverlight 中的 RIA 服务结合构建和验证数据的最佳实践或广泛接受的方法?

这是我的问题的症结所在。假设我有一个 EmployeeView、EmployeeViewModel 和一些 Employee 实体。在常规 RIA 应用程序中,我将在视图上公开 Employee 实体,并且“免费”获得验证,因为实体实现了 INotifyDataErrorInfo 和 IDataErrorInfo(正确吗?)。

现在,如果我想通过 ViewModel 而不是直接通过实体公开一些 Employee 属性,那么它会变得更加复杂。我可以直接公开我需要的位并将它们挂接到后端的实体中,如下所示:

    private Employee _employee;

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

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

...但我失去了实体的美味“免费”验证。否则,我可以直接在视图模型中公开实体,就像这样

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

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

在这种情况下,视图将直接绑定到 Employee 实体并在其中找到它的属性,如下所示:

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

使用此方法我们可以获得“免费”验证,但它是不完全是 MVVM 的干净实现。

第三种选择是在虚拟机中自己实现 INotifyDataErrorInfo 和 IDataErrorInfo ,但这看起来像是大量的管道代码,考虑到我使用上述解决方案是多么容易,并且有一些稍微不太“干净”但很糟糕的东西归根结底,事情变得容易多了。

所以我想我的问题是,这些方法中哪种适合哪种情况?我缺少更好的方法吗?

如果它是相关的,我正在查看 Caliburn.Micro MVVM 框架,但我很想看到普遍适用的答案。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

-残月青衣踏尘吟 2024-10-10 09:04:53

我正在将 RIA 与 Caliburn.Micro 结合使用,并且对我的客户端验证解决方案非常满意。

我所做的是将 ValidationBaseViewModel 放在 Screen (由 Caliburn.Micro 提供)和我的实际应用程序 VM(在您的情况下是 EmployeeViewModel)之间。 ValidationBaseViewModel 实现了 INotifyDataErrorInfo,以便您谈论的管道代码只编写一次。然后,我通过 ValidationBaseViewModel 从 (Caliburn.Micro) PropertyChangedBase.NotifyOfPropertyChange 的覆盖添加/删除/通知错误,代码如下:

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);
}

这实际上是在另一个虚拟机中( ValidationBaseViewModel 和 EmployeeViewModel 之间),具有以下定义:

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

其中 Entity 是 RIA System.ServiceModel.DomainServices.Client.Entity_editing 类成员是当前虚拟机正在编辑的此类型 TEdit 的实例。

与 Caliburn 协程结合使用,我可以做一些很酷的事情,如下所示:

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

    ...
}

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;
    }

    ...
}
橘味果▽酱 2024-10-10 09:04:53

如果您不想使用外部资源或框架,那么我可以有一个实现 INotifyDataErrorInfoViewModelBase

该类将使用 ValidateProperty(string propertyName, object value) 来验证特定属性,并使用 Validate() 方法来验证整个对象。内部使用 验证器 类返回 ValidationResult
如果您使用反射器,则可以通过模仿 Entity 类本身到 ViewModelBase

虽然它不是“免费”,但仍然相对便宜。

以下是 IDataErrorInfo 的示例实现。虽然没有经过测试,但会给你这个想法。

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));
  }
}

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));
  }
}
一页 2024-10-10 09:04:53

您可以使用分部类来扩展您的实体并通过 idataerrorinfo 添加数据验证。

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文