Silverlight, Validation and MVVM - Part II 提交时验证

Home > View Post

Silverlight, Validation and MVVM - Part II

The new Validation states for controls in Silverlight 3 sure look nice but there are a number of limitations. For starters, you can only invoke them through:

  • An exception thrown by a bound property setter
  • An exception thrown by a ValueConverter
The latter feels particularly unsavoury as you'll need to reuse the same converter wherever you want to bind to that property - a big violation of the DRY principle.

You can't even use the new ValidationAttributes from System.ComponentModel.DataAnnotations. Well, actually you can but you'd have to this inside the setter:

[Range(18, int .MaxValue, ErrorMessage="Must be 18 or over" )]
public int Age
{
    get { return _age; }
    set
    {
        Validator.ValidateProperty(value, new ValidationContext(this , null , null )
        {
            MemberName = "Age" ,
        });
        _age = value;
        OnPropertyChanged("Age" );
    }
}

(You can imagine an enhancement to this pattern using the SetValue concept I shared with my new snippets the other day).

What's particularly tricky is to display the invalid state if the value is never changed by the user. For example, you have a name field that is required:

[Reguired(ErrorMessage="Name is required" )]
public string Name

This is bound to a TextBox:

< TextBox Text ="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" x:Name ="NameTextBox" />
< Button Content ="Save" x:Name ="SaveButton" />

The problem is, the user might click save without ever entering the a name. Oh no!

Sure, we can catch that in our code (using the Validator type again, for example). However, there's no easy way of forcing the control to display the error. The only way to achieve this is to force the binding to update programmatically, like so:

BindingExpression binding = NameTextBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();

Now, to get this working on any scale is going to require code-behind. Lots of code-behind. And everybody knows I hate this. The whole validation story at the moment isn't going to play at all well with Model-View-ViewModel (MVVM).

What we need is an easy way to Update all bindings from our ViewModel...

And so, I set about finding a more declarative way of achieving solving. My approach builds on my previous post Silverlight Validation and ViewModel .

Re-introducing the ValidationScope

In this update - the ValidationScope class becomes much more important and has more to offer than just an attached properties/behavior.

It now becomes an integral part of your ViewModel. Let's walk through a scenario. Here we'll have a ViewModel that exposes a Person property of type Person:

// properties snipped down for brevity
public class Person
{
    [Required(ErrorMessage= "Name is required" )]
    public string Name {}

    [Required(ErrorMessage = "Salutation is required" )]
    public string Salutation {}

    [Range(0, int .MaxValue, ErrorMessage = "Must be over 18" )]
    public int Age {}
}

Nice and easy. Now the validation scope comes into play - we add an instance to our ViewModel because we'll access it via binding. To be honest, this could go almost anywhere you like provided it's accessible in a Binding (resources, in the Person class itself, anywhere you like!).

// properties snipped down for brevity
public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<string > Salutations {}
    public Person Person {}
    public ValidationScope PersonValidationScope {}
}

So there's our model and our ViewModel. Now for some view - our Xaml (again, simplified for brevity):

<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">
        < TextBox
            Text ="{Binding Person.Name, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
            local:ValidationScope.ValidateBoundProperty="Text" />
        < TextBox
            Text ="{Binding Person.Age, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
            local:ValidationScope.ValidateBoundProperty="Text" />
        < ComboBox
            ItemsSource ="{Binding Salutations}"     
         SelectedItem ="{Binding Person.Salutation, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"
    local:ValidationScope.ValidateBoundProperty="SelectedItem" />
    < Button Content ="Save" Click ="SaveButtonClick" />
</ StackPanel >

And the code-behind:

//Note - I'd normally use Prism's DelegateCommand and commanding support to avoid this code-behind but don't want to muddy the example
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
_personViewModel.Save();
}

Finally, the Save method on the ViewModel

public void Save()
{
    // This causes all registered bindings to be updated
    PersonValidationScope.ValidateScope();
    if (PersonValidationScope.IsValid())
    {
        // Save changes!
    }
}

How it all works

Whilst getting here took me a whole morning of confusion - it's actually quite straightforward.

First, we use an attached behavior to pass the FrameworkElement we want to be the conceptual 'validation scope' within the VisualTree to our actual ValidationScope instance:

<StackPanel local:ValidationScope.ValidationScope="{Binding PersonValidationScope}">

Then we specify the property who is bound and might need a refresh for each control :

<TextBox local:ValidationScope.ValidateBoundProperty="Text" />
<ComboBox local:ValidationScope.ValidateBoundProperty="SelectedItem" />

Sadly, this is really a violation of the DRY principle anyway but it does have the added advantage of having to opt in your Bindings to the ValidationScope. Finally, when we're ready, we tell the ValidationScope to update all the bindings:

PersonValidationScope.ValidateScope();

This kicks the process into action with a crawl of the VisualTree inside the FrameworkElement registered as our scope (the StackPanel in this case) and hunts out any attached ValidateBoundProperty properties wired to controls. When it finds them it looks for the appropriate DependencyProperty (Text and SelectedItem in our demo)

demoware

If you're interested in this you'll want to see a working demo and the source code. Here's the former and the latter will follow in a minute. To bush the barrier, we actually have two validation scopes in the same view, side-by-side

 

Before you get to the source - bear in mind this is demoware/spike standard only and probably needs work. The usual disclaimers apply. Here are just some thing that haven't even been thought about in the current implementation:
  • binding to the same ValidationScope from two different FrameworkElements
  • performance could be improved, quite a bit of reflection
  • nested ValidationScopes
  • many more I'm sure
Finally, before you rush off make sure to double check that the new DataForm control and/or .NET RIA Services isn't really what you need to solve your validation requirements.

The source

Get it here and remember, this may damage your health and your house is at risk if you use it as it is: Download Source (15 KB)

UPDATE: Be sure to go on and read Part III in this series!

 

Josh Post By Josh Twist
07:51
31 Jul 2009

» Next Post: How to work with PropertyChanged's smelly name string
« Previous Post: New snippets for Silverlight and WPF

Comments:

Posted by Chad @ 02 Aug 2009 00:27
Thanks for your post. Have been thinking about how to tackle this problem elegantly in my own app atm.

Posted by Jonathan @ 06 Aug 2009 11:17
Nice example. This gets pretty close to what I was looking for but (I havent actually run your code yet) I think that when your ValidationScope calls UpdateSource() for the FrameworkElement, there could be some issues that cause it to not work properly. Specifically if you have some of your items in different TabItems in a TabControl and the user tries to save, if the user hasn't actully viewed some of the TabItems, the UpdateSource for those unviewed controls (TextBox, etc) wont do anything. I think this relates to the PropertyPathListener not being initialized in the OnAttach property within the BindingExpression class. Any ideas for this? I am almost the point of abandoning BindingExpression and just setting ToolTip on each control for my errors.

Posted by josh @ 07 Aug 2009 04:58
That's an interesting point and I haven't tried this.

I guess the display wouldn't need to update if you can't see the control though and the important thing would be to make sure full validation occurs before update.

e.g.

PersonValidationScope.ValidateScope();
if (PersonValidationScope.IsValid() && Validator.ValidateObject(this.Person))
{
// Save changes!
}


Make sense?

Posted by tomas.k @ 17 Aug 2009 07:31
Great article,
I looking for something similar for some time. Thank you for sharing it!

Posted by kanur @ 17 Aug 2009 13:43
Other problem I am solving is localization of messages from ValueConverter. I am not able to localize message: "Input is not in correct format".
I tried my custom ValueConverter but there is not possible to throw exception to be caught by UI.

Any ideas?
Thanks

Posted by Mark @ 20 Aug 2009 00:56
Hi Josh, I finally got round to implementing this. One question came about fairly quickly: how would you deal with optional fields, that need to be validated only when information is entered? Thanks, Mark

Posted by Mark @ 20 Aug 2009 00:56
Hi Josh, I finally got round to implementing this. One question came about fairly quickly: how would you deal with optional fields, that need to be validated only when information is entered? Thanks, Mark

Posted by Paurav @ 27 Aug 2009 14:12
Hi Josh,

Really like this solution. Thanks.

Mark, you can use a custom validator attribute on the property you are validating.

Posted by roopesh @ 31 Oct 2009 01:35
Its really help fulll ...can we do same validation in ria services??????????????? how about custom validation?

Posted by R4cOOn @ 18 Nov 2009 00:05
This is the only post that I found that dealt cleanly with the issue of the validation occurring in the ViewModel.
I share your belief in the "no code-behind" and I was not looking forward to adding a lot of it.

My only gripe is that I'd rather had the class separated in a Validator and a ValidationScopeBahavior in much the same way as the commands and their associated behavior are done in PRISM.
I couldn't get it to work though because I couldn't keep hold of the dependency object if the classes are separated (I could call Validate() but then the DependencyObject containing the ScopeElement wouldn't be there).


Good job!

Posted by Aaron @ 18 Jan 2010 13:20
Nice! But I still think IsValid should be supplied to us out of the box :)

Posted by Antti Makkonen @ 26 Jan 2010 04:54
Thanks for great sample.

I am now stuck with how to actually localize validation messages using ValueConverter or similar approach.

Any ideas?

Posted by Ken @ 18 Feb 2010 15:18
Nice! This is the only way to go in silverlight 3. The DataForm is useless in my opinion.

Posted by Dmitriy @ 08 Mar 2010 07:10
Thanks for the great article!
In your example the error message for the salutation combobox suppossed to be (ErrorMessage = "Salutation is required") yet it shows "Input is not in a correct format."
I ran into similar issue with validating combobox in my application. Any ideas how to overcome that?

Posted by Nirmal @ 27 Mar 2010 06:21
Hai,

i am using radgridview. While editing the cell details i am using radGridView_CellValidating event.

In that,

void radGridView_CellValidating (object sender, Telerik.Windows.Controls.GridViewCellValidatingEventArgs e)

{

e.IsValid = false;

e.ErrorMessage ="Invalid Data(Some Message)";

}

It shows the error message when mouse over on the right top corner of tht red indication. But i need when focusing on that cell or immediate when error occurs(doesn't need tooltipservice). How can i do this?

Posted by boldtechie @ 14 Apr 2010 12:46
Hi, I was trying the validation with datagrid in silverlight 3. But my error messages was not showing up when i hit save. Is there any thing else i have to do in code?
I debugged through code, found that just like any normal textbox control in the page the be.UpdateSource(); will be called. but my text box was not showing up the error messages when its inside datagrid.

Posted by Eric @ 05 Jun 2010 05:26
I have the same problem as boldtechie . Are there any known problem if we use TemplatePanels and custom controls?

Posted by TM @ 02 Aug 2010 12:31
Thank you, Josh. Really good input - just what I needed!

Posted by cwobie @ 06 Oct 2010 13:37
Hi Josh, Excellent post and it works great. This is my requirement, I must have a datagrid where by users can enter values for the columns, the add a new row to the grid by adding a new blank object to the ObservableCollection. After implementing your code and hitting the save button it works great. No the problem, the second requirement is to import data from a file into this DataGrid. No big deal, i read each row from the file, populate a new object by assigning the values to the properties and add it to the collection.
But when i then hit the save button, none of the fields populated from the file gets validated, only the blank one i created first gets validated. AS long as I dont assign values to the new object being created (as I do when adding a new row) all is fine, but not when i populate the object from the file and adding it to the collection.

Will appreciate it greatly if you can shed some light on this or propose a solution.

Posted by Alfonso Paredes @ 04 Nov 2010 19:30
unhandled exception in your demo code at

Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName });

at method
protected virtual bool SetValue<T>(ref T target, T value, string propertyName)

from class ModelBase

Posted by Mitesh Patel @ 10 Nov 2010 16:03
This is really good example, particularly in MVVM patern for validation its really helpful, but still I am looking for the solution of my problem where if I am using String formatting on my text box, its causing problem for the behavior which is implemented for textbox. We have the behavior which bind the source with textbox on textchange/Keyup event. But if this behavior is ON then cursor in textbox jumping to first posion after each keystroke. So, i am looking for the solution... If you have any suggestion please send me on miteshpatel2011@gmail.com

Thanks.

Posted by Jag @ 09 Dec 2010 16:39
Hey Josh,

Thanks a lot for this lovely post. It is working brilliantly for me. You have saved me few hours :-)

Cheers---Jag

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值