This may get a tad long, but here goes. I have created a small, wizard-style sample app using the MVVM pattern (basically a dumbed-down version of the code in my "real" app). In this app, the main window moves from through a List<..> of view models, with each view model displaying its associated view. I have 2 view model classes that are essentially identical, and they display the same view.
On the view is a combo box, populated with an array of float. The SelectedItem is bound to a float property on the view model. I have created a template for the combo box to display each item as a TextBlock, with the text taking the float value and going through a value converter.
The problem, when I switch back and forth between view models, all works fine as long as every view model I switch to is of the same class. As soon as I change the current page to an instance of a different view model, the value converter's Convert gets called with a 'value' parameter that is a zero-length string. Up til then, Convert was only being called with a float, as I would expect.
My question : why is the converter being called with the empty string ONLY in the case of switching view model classes?
I am attaching the main window XAML and view model, as well as the view/view models displayed for each "page". You'll notice that the main window view model has a list containing 2 instances of PageViewModel and 2 instances of OtherViewModel. I can switch back and forth between the first 2 fine, and the value converter only gets called with a float value. Once I switch to the first OtherViewModel instance, the converter gets an "extra" call with an empty string as the value.
Code snippets :
MainWindow
<Grid.Resources>
<DataTemplate DataType="{x:Type local:PageViewModel}">
<local:PageView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:OtherViewModel}">
<local:PageView />
</DataTemplate>
</Grid.Resources>
<!-- Page -->
<ContentControl Margin="5,5,5,35"
Height="100"
IsTabStop="False"
Content="{Binding CurrentPage}" />
<!-- Commands -->
<Button Margin="5,115,0,0"
Width="75"
Content="< Back"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Command="{Binding BackCommand}" />
<Button Margin="85,115,0,0"
Width="75"
Content="Next >"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Command="{Binding NextCommand}" />
MainWindowViewModel
public MainWindowViewModel()
{
m_pages = new List<BaseViewModel>();
m_pages.Add(new PageViewModel(1, 7f));
m_pages.Add(new PageViewModel(2, 8.5f));
m_pages.Add(new OtherViewModel(3, 10f));
m_pages.Add(new OtherViewModel(4, 11.5f));
m_currentPage = m_pages.First();
m_nextCommand = new BaseCommand(param => this.OnNext(), param => this.EnableNext());
m_backCommand = new BaseCommand(param => this.OnBack(), param => this.EnableBack());
}
// Title
public string Title
{
get
{
return (CurrentPage != null) ? CurrentPage.Name : Name;
}
}
// Pages
BaseViewModel m_currentPage = null;
List<BaseViewModel> m_pages = null;
public BaseViewModel CurrentPage
{
get
{
return m_currentPage;
}
set
{
if (value == m_currentPage)
return;
m_currentPage = value;
OnPropertyChanged("Title");
OnPropertyChanged("CurrentPage");
}
}
// Back
ICommand m_backCommand = null;
public ICommand BackCommand
{
get
{
return m_backCommand;
}
}
public void OnBack()
{
CurrentPage = m_pages[m_pages.IndexOf(CurrentPage) - 1];
}
public bool EnableBack()
{
return CurrentPage != m_pages.First();
}
// Next
ICommand m_nextCommand = null;
public ICommand NextCommand
{
get
{
return m_nextCommand;
}
}
public void OnNext()
{
CurrentPage = m_pages[m_pages.IndexOf(CurrentPage) + 1];
}
public bool EnableNext()
{
return CurrentPage != m_pages.Last();
}
}
Notice the 2 instance of one view model followed by 2 instances of the other.
PageView
<Grid.Resources>
<x:Array x:Key="DepthList"
Type="sys:Single">
<sys:Single>7</sys:Single>
<sys:Single>8.5</sys:Single>
<sys:Single>10</sys:Single>
<sys:Single>11.5</sys:Single>
</x:Array>
<local:MyConverter x:Key="MyConverter" />
</Grid.Resources>
<TextBlock Text="Values:"
Margin="5,5,0,0">
</TextBlock>
<ComboBox Width="100"
Height="23"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="5,25,0,0"
DataContext="{Binding}"
SelectedItem="{Binding Depth}"
ItemsSource="{Binding Source={StaticResource DepthList}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource MyConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
PageViewModel/OtherViewModel/MyConverter
public class PageViewModel : BaseViewModel
{
public PageViewModel(int index, float depth)
{
Depth = depth;
Name = "Page #" + index.ToString();
}
public float Depth
{
get;
set;
}
}
public class OtherViewModel : BaseViewModel
{
public OtherViewModel(int index, float depth)
{
Depth = depth;
Name = "Other #" + index.ToString();
}
public float Depth
{
get;
set;
}
}
[ValueConversion(typeof(DateTime), typeof(String))]
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Debug.WriteLine("IValueConverter.Convert : received a " + value.GetType().Name);
string text = "";
if (value is float)
{
text = value.ToString();
}
else
{
throw new ArgumentException("MyConverter : input value is NOT a float.");
}
return text;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return float.Parse(value as string);
}
}
Note: I can remove the exception in the Convert method, and everything seems to work fine. But, I would like to know why this is happening. Why is the converter getting an empty string instead of the expected float, and only when we switch view models?
Any insights would be greatly appreciated. Thanks in advance...
Joe
I've had the same issue (with an enum instead of a float).
When the View is closed the ComboBox Selection is emptied. You can check this by handling SelectionChanged event and inspecting the SelectionChangedEventArgs RemovedItems collection.
This ends in String.Empty being passed into your ValueConverter.
In my case, I have modified the ValueConverter.Convert to allow string.Empty as a valid value, and return string.Empty.
This is the code I used:
// When view is unloaded, ComboBox Selection is emptied and Convert is passed string.Empty
// Hence we need to handle this conversion
if (value is string && string.IsNullOrEmpty((string)value))
{
return string.Empty;
}
Try
public BaseViewModel
{
public virtual float Depth{get;set;}
...
}
Then
public class PageViewModel : BaseViewModel
{
...
public override float Depth { get; set; }
}
and
public class OtherViewModel : BaseViewModel
{
...
public override float Depth { get; set; }
}
Then you only need one DataTemplate
<Grid.Resources>
<DataTemplate DataType="{x:Type local:BaseViewModel}">
<local:PageView />
</DataTemplate>
</Grid.Resources>
I'm guessing the strange value being passed to the converter is due to DataTemplates being switched.
Not tested
Related
I have an application with a TreeView control that is built using a set of data types representing different levels in the hierarchy and an accompanying set of HierarchicalDataTemplates. What I want to do now is set appropriate AutomationProperties.Name values on the tree items.
Normally, I would use TreeView.ItemContainerStyle to bind the accessible name, but this is rather limited, as it requires I use a binding path that works for all types.
In this case, however, I would much rather be able to control the accessible name independently for each type. For example, it may be useful to include Id in some layers, but not in others.
I could probably live with using the displayed text, but while I can easily use a RelativeSource binding in TreeView.ItemContainerStyle to get at the TreeViewItem, the Path needed to ultimately reach the TextBlock.Text value in the templated item from there eludes me.
I have also tried using HierarchicalDataTemplate.ItemContainerStyle, but that only applies to child items. Even further, when I tried to define it on each template, only BazItems were properly set, even though I would have expected BarItems to work as well.
I put together a minimal example to illustrate the issue. The item types are as follows:
public sealed class BazItem
{
public BazItem(int id, string name)
{
Id = id;
Name = name ?? throw new ArgumentNullException(nameof(name));
}
public int Id { get; }
public string Name { get; }
}
public sealed class BarItem
{
public BarItem(int id, string display)
{
Id = id;
Display = display ?? throw new ArgumentNullException(nameof(display));
Bazs = new ObservableCollection<BazItem>();
}
public int Id { get; }
public string Display { get; }
public ObservableCollection<BazItem> Bazs { get; }
}
public sealed class FooItem
{
public FooItem(int id, string name)
{
Id = id;
Name = name ?? throw new ArgumentNullException(nameof(name));
Bars = new ObservableCollection<BarItem>();
}
public int Id { get; }
public string Name { get; }
public ObservableCollection<BarItem> Bars { get; }
}
The corresponding templates are as follows:
<HierarchicalDataTemplate DataType="{x:Type local:BazItem}">
<TextBlock Text="{Binding Name, StringFormat='baz: {0}'}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:BarItem}"
ItemsSource="{Binding Bazs}">
<TextBlock Text="{Binding Display, StringFormat='bar: {0}'}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:FooItem}"
ItemsSource="{Binding Bars}">
<TextBlock Text="{Binding Name, StringFormat='foo: {0}'}"/>
</HierarchicalDataTemplate>
Finally, the tree view in the view is as follows:
<TreeView ItemsSource="{Binding Foos}"/>
where Foos is an ObservableCollection<FooItem> property on the underlying view.
The solution I came up with for now (pending a better answer) is to change the TreeView.ItemContainerStyle to use a value converter against the tree item object rather than a property on the object:
TreeView element:
<TreeView ItemsSource="{Binding ElementName=View, Path=Foos}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="AutomationProperties.Name" Value="{Binding Converter={StaticResource AccessibleConverter}}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Converter:
[ValueConversion(typeof(object), typeof(string), ParameterType = typeof(Type))]
public sealed class AccessibleTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch (value)
{
case FooItem foo:
return $"foo: {foo.Name}";
case BarItem bar:
return $"bar: {bar.Display}";
case BazItem baz:
return $"baz: {baz.Name}";
default:
return Binding.DoNothing;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This works, but it's less-than-ideal for several reasons:
it duplicates the format strings (though I could pull them out into a shared resource location)
the value converter has to first convert object to the appropriate type before it can come up with the appropriate string format
adding a new tree item type requires I touch both the templates as well as the value converter
I came along the same issue and the way I solved it was by overriding the ToString() method of the class of the object i was binding the TreeViewItem.
For example, in your case the type of item you bind to TreeViewItem is of type BazItem, go to BazItem class and:
public override string ToString()
{
// This is what the Windows Narrator will use now
return "Name of Baz Item";
}
I have a progress bar that I want to change color depending on a boolean value; true is green and false is red. I have code that seems like it should work (it returns the correct value when I bind it to a textbox) but not when it's the color property of the progress bar. The converter is defined as this (in App.xaml.cs since I want to access it anywhere):
public class ProgressBarConverter : System.Windows.Data.IValueConverter
{
public object Convert(
object o,
Type type,
object parameter,
System.Globalization.CultureInfo culture)
{
if (o == null)
return null;
else
//return (bool)o ? new SolidColorBrush(Colors.Red) :
// new SolidColorBrush(Colors.Green);
return (bool)o ? Colors.Red : Colors.Green;
}
public object ConvertBack(
object o,
Type type,
object parameter,
System.Globalization.CultureInfo culture)
{
return null;
}
}
I then add the following to the App.xaml (so it can be a global resource):
<Application.Resources>
<local:ProgressBarConverter x:Key="progressBarConverter" />
<DataTemplate x:Key="ItemTemplate">
<StackPanel>
<TextBlock Text="{Binding name}" Width="280" />
<TextBlock Text="{Binding isNeeded,
Converter={StaticResource progressBarConverter}}" />
<ProgressBar>
<ProgressBar.Foreground>
<SolidColorBrush Color="{Binding isNeeded,
Converter={StaticResource progressBarConverter}}" />
</ProgressBar.Foreground>
<ProgressBar.Background>
<SolidColorBrush Color="{StaticResource PhoneBorderColor}"/>
</ProgressBar.Background>
</ProgressBar>
</StackPanel>
</DataTemplate>
</Application.Resources>
I added the following to MainPage.xaml to display them:
<Grid x:Name="LayoutRoot" Background="Transparent">
<ListBox x:Name="listBox"
ItemTemplate="{StaticResource ItemTemplate}"/>
</Grid>
And then in MainPage.xaml.cs, I define a class to hold the data and bind it to the listBox:
namespace PhoneApp1
{
public class TestClass
{
public bool isNeeded { get; set; }
public string name { get; set; }
}
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
var list = new LinkedList<TestClass>();
list.AddFirst(
new TestClass {
isNeeded = true, name = "should be green" });
list.AddFirst(
new TestClass {
isNeeded = false, name = "should be red" });
listBox.ItemsSource = list;
}
}
}
I've attached a minimal working example so it can just be built and tested. An image of the output is here:
It returns the values from the converter for the textbox but not the progress bar. When I run the debugger, it doesn't even call it.
Thanks for any help!
Try to modify your converter to return a SolidColorBrush and then bind directly to your ProgressBars Foreground property.
I am fairly new to MVVM, so bear with. I have a view model class that has a public property implemented as so:
public List<float> Length
{
get;
set;
}
In my XAML for the view, I have several text boxes, with each one bound to a specific element in this Length list:
<TextBox Text="{Binding Length[0], Converter=DimensionConverter}" />
<TextBox Text="{Binding Length[2], Converter=DimensionConverter}" />
<TextBox Text="{Binding Length[4], Converter=DimensionConverter}" />
The DimensionConverter is a IValueConverter derived class that formats the values like a dimension (i.e. 480.0 inches becomes 40'0" in the text box on screen), and back again (i.e. takes 35'0" for a string and yield 420.0 inches for the source)
My issue: I need to be able to validate each value in the List as it is changed in the associated TextBox. For some, I may need to modify other values in the List depending on the entered value (i.e. change the float at Length[0] will change the value at Length[4] and update the screen).
Is there any way to re-work the property to allow for an indexer? Or, do I need to create individual properties for each item in the List (which really makes the List unnecessary)? Essentially, since I already have the collection of float, I was hoping to be able to write MVVM code to validate each item as it is modified.
Thoughts? (and, thanks in advance)
You can use an ObservableCollection<float> instead of a List<float>, and handle the CollectionChanged event to detect when the user changes a value.
Wouldn't something like this:
<ItemsControl ItemsSource="{Binding Length}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Mode=TwoWay, Converter=DimensionConverter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Be close to what you want?
It will display the entire list, and allow the user to modify the values, which will be returned straight back to the list, as long as your IValueConverter implements ConvertBack.
Then do as Thomas said to validate, or implement an ObservableLinkedList
What you do at the moment looks dirty already and it's barely a few lines of code..
It would be great if you can have a class which implements INotifyPropertyChanged to have the properties provided the length of list is constant.
if you want validate your text input with mvvm then create a model that you can youse at your viewmodel
public class FloatClass : INotifyPropertyChanged
{
private ICollection parentList;
public FloatClass(float initValue, ICollection pList) {
parentList = pList;
this.Value = initValue;
}
private float value;
public float Value {
get { return this.value; }
set {
if (!Equals(value, this.Value)) {
this.value = value;
this.RaiseOnPropertyChanged("Value");
}
}
}
private void RaiseOnPropertyChanged(string propName) {
var eh = this.PropertyChanged;
if (eh != null) {
eh(this, new PropertyChangedEventArgs(propName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
at your viewmodel you can use the model like this
public class FloatClassViewmModel : INotifyPropertyChanged
{
public FloatClassViewmModel() {
this.FloatClassCollection = new ObservableCollection<FloatClass>();
foreach (var floatValue in new[]{0f,1f,2f,3f}) {
this.FloatClassCollection.Add(new FloatClass(floatValue, this.FloatClassCollection));
}
}
private ObservableCollection<FloatClass> floatClassCollection;
public ObservableCollection<FloatClass> FloatClassCollection {
get { return this.floatClassCollection; }
set {
if (!Equals(value, this.FloatClassCollection)) {
this.floatClassCollection = value;
this.RaiseOnPropertyChanged("FloatClassCollection");
}
}
}
private void RaiseOnPropertyChanged(string propName) {
var eh = this.PropertyChanged;
if (eh != null) {
eh(this, new PropertyChangedEventArgs(propName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
here is the xaml example
<ItemsControl ItemsSource="{Binding Path=FloatClassViewmModel.FloatClassCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Value, Mode=TwoWay, Converter=DimensionConverter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
hope this helps
While looking at solutions for tying an enum to a group of RadioButtons, I discovered Sam's post from a year and a half ago.
Lars' answer was exactly what I was looking for: simple and effective.
Until I started changing the object tied to the RadioButton group. A simple version follows.
The XAML:
<Window x:Class="RadioEnum.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:re="clr-namespace:RadioEnum"
Height="200" Width="150">
<Window.DataContext>
<re:ViewModel />
</Window.DataContext>
<Window.Resources>
<re:EnumBooleanConverter x:Key="enumBooleanConverter" />
</Window.Resources>
<DockPanel>
<ComboBox DockPanel.Dock="Top" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Things}" DisplayMemberPath="Name" />
<GroupBox>
<StackPanel>
<RadioButton IsChecked="{Binding Path=Things/Choice, Converter={StaticResource enumBooleanConverter}, ConverterParameter=First}">First</RadioButton>
<RadioButton IsChecked="{Binding Path=Things/Choice, Converter={StaticResource enumBooleanConverter}, ConverterParameter=Second}">Second</RadioButton>
<RadioButton IsChecked="{Binding Path=Things/Choice, Converter={StaticResource enumBooleanConverter}, ConverterParameter=Third}">Third</RadioButton>
</StackPanel>
</GroupBox>
</DockPanel>
</Window>
Now, the C#:
namespace RadioEnum
{
public class ViewModel {
public ObservableCollection<Thing> Things { get; set; }
public ViewModel() {
Things = new ObservableCollection<Thing> {
new Thing{ Name = "Thing1", Choice = Choice.First, },
new Thing{ Name = "Thing2", Choice = Choice.Second, },
};
}
}
public class Thing {
public string Name { get; set; }
public Choice Choice { get; set; }
}
public enum Choice { None, First, Second, Third, }
public class EnumBooleanConverter : IValueConverter {
// Yes, there are slight differences here from Lars' code, but that
// was to ease debugging. The original version has the same symptom.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
object ret = DependencyProperty.UnsetValue;
var parameterString = parameter as string;
if (parameterString != null && Enum.IsDefined(value.GetType(), value)) {
object parameterValue = Enum.Parse(value.GetType(), parameterString);
ret = parameterValue.Equals(value);
}
return ret;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
object ret = DependencyProperty.UnsetValue;
var parameterString = parameter as string;
if (parameterString != null && !value.Equals(false))
ret = Enum.Parse(targetType, parameterString);
return ret;
}
}
}
When the application loads with Thing1 in the ComboBox, the correct Choice is selected in the radio group. Selecting Thing2 from the ComboBox correctly updates the Choice. But, from this point, switching no longer updates the binding to the Second RadioButton and thus no longer calls the Convert method with parameter set to "Second".
In other words, although Thing2's values have not changed, all of the RadioButtons are cleared from that point forward. Thing1 continues to work, though.
There are no errors seen - neither exceptions nor messages in the Output window. I've tried binding in different ways. I tried making Choice a DependencyProperty, too (and Thing then a DependencyObject).
Any insights out there?
Original Response:
Not sure if this will fix your problem or not... as I think the break in the binding might be somewhere with your combobox... but to improve on your EnumConverter and make sure it's working properly... I suggest taking a look at my response to this question:
How to bind RadioButtons to an enum?
(Not the selected answer... but my response with the generic converter rather than converting string values)
Edit:
I just took your code and tried it and everything seemed to work great! (using visual studio 2010 .net 4)
You have a list of Things (in your combobox) and can set the currently selected Thing's choice via radio button. I can modify each Thing's choice and when I switch between Things it correctly updates the radio button for me!
Correct me if I am wrong on the desired functionality:
App Loads - ComboBox: Thing1 RadioButton: First
Select Thing2 - ComboBox: Thing2 RadioButton: Second
Select Thing1 - ComboBox: Thing1 RadioButton: First
Select Third - ComboBox: Thing1 RadioButton: Third
Select Thing2 - ComboBox: Thing2 RadioButton: Second
Select First - ComboBox: Thing2 RadioButton: First
Select Thing1 - ComboBox: Thing1 RadioButton: Third
Select Thing2 - ComboBox: Thing2 RadioButton: First
Above is the functionality I get when running your app with the code you provided (and with the modified EnumConverter). This also appears to be the desired result. Is the above correct and does that not work that way for you?
Edit 2: I can confirm that the issue is with .NET 3.5
I run .NET 4 Client profile... everything works as desired... running .NET 3.5 Client profile... I get the result you stated.
For those of you who may be stuck with doing this in .NET 3.5, I do have something working. It's not nearly as elegant as the code above, but it functions.
I'm more than happy to see some feedback from others on alternative methods, too. The example code below is for a ThingB that functions in both .NET 3.5 and 4.
First, change the XAML on the RadioButtons as follows (note that the GroupName must be different for each):
<RadioButton GroupName="One" IsChecked="{Binding Path=Things/ChoiceOne}">First</RadioButton>
<RadioButton GroupName="Two" IsChecked="{Binding Path=Things/ChoiceTwo}">Second</RadioButton>
<RadioButton GroupName="Three" IsChecked="{Binding Path=Things/ChoiceThree}">Third</RadioButton>
Second, the ThingB code:
public class ThingB : INotifyPropertyChanged {
public string Name { get; set; }
public Choice Choice {
get {
return choiceOne ? Choice.First
: choiceTwo ? Choice.Second
: choiceThree ? Choice.Third : Choice.None;
}
set {
choiceOne = Choice.First.Equals(value);
choiceTwo = Choice.Second.Equals(value);
choiceThree = Choice.Third.Equals(value);
}
}
private bool choiceOne;
public bool ChoiceOne {
get { return choiceOne; }
set {
if(value) {
Choice = Choice.First;
NotifyChoiceChanged();
}
}
}
private bool choiceTwo;
public bool ChoiceTwo {
get { return choiceTwo; }
set {
if (value) {
Choice = Choice.Second;
NotifyChoiceChanged();
}
}
}
private bool choiceThree;
public bool ChoiceThree {
get { return choiceThree; }
set {
if (value) {
Choice = Choice.Third;
NotifyChoiceChanged();
}
}
}
private void NotifyChoiceChanged() {
OnPropertyChanged("ChoiceOne");
OnPropertyChanged("ChoiceTwo");
OnPropertyChanged("ChoiceThree");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
I've got a small test WPF MVVM application working in which a view allows the user to change the first or last names of customers and the full name automatically changes, so communication is going from M-to-MV-to-V and back, everything is fully decoupled, so far so good.
But now as I look to how I will begin extending this to build large applications with the MVVM pattern, I find the decoupling to be an obstacle, namely:
how will I do validation messages, e.g. if back in the model in the LastName setter I add code that prevents names over 50 characters from being set, how can I send a messsage to the view telling it to display a message that the name was too long?
in complex applications I may have dozens of views on a screen at one time, yet I understand that in MVVM each view has one and only one ViewModel assigned to it to provide it with data and behavior, so how is it that the views can interact with each other, e.g. in the above validation example, what if back in the customer model we want to inform a particular "MessageAreaView" to display the message "Last name may only contain 50 characters.", how to we communicate that up the stack to that particular view?
CustomerHeaderView.xaml (View):
<UserControl x:Class="TestMvvm444.Views.CustomerHeaderView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<StackPanel HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding Path=Customers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBox
Text="{Binding Path=FirstName, Mode=TwoWay}"
Width="100"
Margin="3 5 3 5"/>
<TextBox
Text="{Binding Path=LastName, Mode=TwoWay}"
Width="100"
Margin="0 5 3 5"/>
<TextBlock
Text="{Binding Path=FullName, Mode=OneWay}"
Margin="0 5 3 5"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Customer.cs (Model):
using System.ComponentModel;
namespace TestMvvm444.Model
{
class Customer : INotifyPropertyChanged
{
public int ID { get; set; }
public int NumberOfContracts { get; set; }
private string firstName;
private string lastName;
public string FirstName
{
get { return firstName; }
set
{
if (firstName != value)
{
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName
{
get { return lastName; }
set
{
if (lastName != value)
{
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName
{
get { return firstName + " " + lastName; }
}
#region PropertChanged Block
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
}
}
For validation, have your view model implement IDataErrorInfo. As for communication between views, don't be afraid to write UI services (eg. a message service that allows and view model to contribute messages that will be displayed somewhere in the UI). Or if there is a hard relationship between view models (eg. one view model owns another) then the owning view model can hold a reference to the child view model.
A really simple way to add validation messages is to use binding.
Add an notifable property to your view model that defines whether the validation message should be displayed or not:
private Boolean _itemValidatorDisplayed;
public Boolean ItemValidatorDisplayed
{
get { return _itemValidatorDisplayed; }
set
{
_itemValidatorDisplayed= value;
_OnPropertyChanged("ItemValidatorDisplayed");
}
}
Add a convertor class that converts bool to visibility:
using System;
using System.Windows;
namespace xxx
{
public class BoolToVisibilityConverter : IValueConverter
{
public bool Negate { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool val = System.Convert.ToBoolean(value);
if (!Negate)
{
return val ? Visibility.Visible : Visibility.Collapsed;
}
else
{
return val ? Visibility.Collapsed : Visibility.Visible;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Bind to the property from the view and apply the converter:
<UserControl x:Class="ViewClass"
...
>
<UserControl.Resources>
<contract:BoolToVisibilityConverter Negate="False"
x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>
...
<TextBlock Visibility="{Binding Converter={StaticResource BoolToVisibilityConverter}, Path=ItemValidatorDisplayed}" />
...
</UserControl>
You'll need to be setting the ViewModel as the datacontext of the view:
namespace xxx
{
public partial class ViewClass: UserControl
{
public ViewClass()
{
InitializeComponent();
this.DataContext = new ViewClass_ViewModel();
}
}
}
Bingo - perfectly working validation being pushed to any view that cares to subscribe to this ViewModel / Property.
You will also be able to bind validation to the source object collection in SL3 :-)