WPF MVVM with mvvmlight and Fluent.Validation - c#

I am having a little bit of a conundrum with my newest App.
It's a Master-Detail WPF MVVM App that uses MVVM Light and Fluent.Validation.
The View's DataContext is a MainViewModel : ViewModelBase with an ObservableCollection<ProviderDto> for the ListView on the left and a property ProviderDto SelectedProvider for the detailed Properties on the right.
There are also several RelayCommands to Add, Edit and Delete single ProviderDto's
The ViewModel uses a ProviderService to perform These Actions, which is injected in it's constructor with mvvmlight's SimpleIoC in a separate ViewModelLocator.
Everything works fine so far, I also managed to have Design-Time-Data.
I now tried to add Fluent.Validation to the Mix and implemented like it is described in this post (My ProviderDto now inherits from a ValidationBase instead of ObservableObject. The Base now inherits from ObservableObject. Also I registered the ProviderDtoValidator in the ViewModelLocator.)
This allows me to have my ObservableObjects have automatically validated and to call .IsValid on them.
So far so good, I am sure I will be able to make it work up to the View and make those error boxes get red :).
Now to my real question:
I want to have a Button on the View to Save the changes on the SelectedProvider. This should naturally be bound to this:
Relaycommand SaveProviderCommand = new RelayCommand(SaveProvider, CanSaveProvider)
private bool CanSaveProvider()
{
return SelectedProvider.IsValid;
}
private void SaveProvider()
{
if (SelectedProvider.IsValid)
_providerController.SaveProvider(SelectedProvider);
}
Where do I put the SaveProviderCommand SaveCommand?
If I put it in the ViewModel then I can only call it from the SelectedProvider-Property:
public ProviderDto SelectedProvider
{
get { return _selectedProvider; }
set
{
Set(() => SelectedProvider, ref prV_selectedProvider, value);
SaveProviderCommand.RaiseCanExecuteChanged(); // Here!
}
}
Which obviously doesn't work when just a single property in the SelectedProvider gets changed.
The other possibility is to put the Command on the DTO itself and call it everytime a property gets Changed. For instance when the Email-Property is changed:
//A Property from Provider
public string Email
{
get { return _email; }
set
{
Set(() => Email, ref _email, value.TrimSafe());
SaveProviderCommand.RaiseCanExecuteChanged(); // Here!
}
}
The Advantage here is that the Validation works out-of-the-box up to the View-Level when I Change every property. The disadvantage is that I would have to inject the ProviderController in the DTO's constructor so it can be called in the private Save-Method. I don't think the DTO should know how to save itself. It only should be able to tell if it .IsValid and the Saving logic should belong to the ViewModel.
I hope you can see my Dilemma:
If I put the SaveCommand in the ViewModel, then I will have to do
I-dont-know-what to validate my SelectedProvider in the View. How would the Validation work? I looked into DataTemplating the Controls but I don't seem to be able to make it work together with Fluent.Validation..
If I put SaveCommand in the DTO itself then the Validation works nicely but I don't think it's correct to inject so many capabilities in something that should stay dumb.
Of course this is a boiled-down example, but I think it's enough to illustrate the Problem. Hope to get some good advice on Patterns and practices.

I found a suitable solution for this Problem, maybe it's going to help somebody else.
Since both the SelectedProvider in the Viewmodel and the single Properties of it implement INotifyPropertyChanged (trhough ViewModelBase or ObservableObject) I can simply subscribe to the SelectedProvider.PropertyChanged in the ViewModel.
public MainViewModel()
{
// Constructor
if (SelectedProvider != null)
SelectedProvider.PropertyChanged += SelectedProvider_PropertyChanged;
}
private void SelectedProvider_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
SaveProviderCommand.RaiseCanExecuteChanged();
}
In the View I can implement the Controls according to this post
<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<Grid DockPanel.Dock="Right" Width="16" Height="16"
VerticalAlignment="Center" Margin="3 0 0 0">
<Ellipse Width="16" Height="16" Fill="Red"/>
<Ellipse Width="3" Height="8"
VerticalAlignment="Top" HorizontalAlignment="Center"
Margin="0 2 0 0" Fill="White"/>
<Ellipse Width="2" Height="2" VerticalAlignment="Bottom"
HorizontalAlignment="Center" Margin="0 0 0 2"
Fill="White"/>
</Grid>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource=
{x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<TextBox Name="TxtEmail" Margin="5" Text="{Binding Path=SelectedProvider.Email,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, Mode=TwoWay}" />
This Approach gives me the Separation of Concerns that I wanted and the Nice out-of-the-box Validation. The only small disadvantage: I don't find it very aesthetical to have an Event-Subscription in the VM...

Related

Avalonia style selector doen't works on derived classes

I'm having an issue to style my custom control derived from button.
I inherited the Button class to add a DependencyProperty to it:
public class IconButton : Button, IStyleable
{
Type IStyleable.StyleKey => typeof(Button);
public static readonly StyledProperty<string> IconProperty =
AvaloniaProperty.Register<IconButton, string>(nameof(Icon));
public string Icon
{
get { return GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
}
Now I want to create a style targetting only this derived class:
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:cc="*****.*****.CustomControls">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Orientation="Vertical">
<cc:IconButton Content="hello custom" Icon="fab fa-github"/>
<Button Content="hello"/>
</StackPanel>
</Border>
</Design.PreviewWith>
<Style Selector=":is(cc|IconButton)">
<Setter Property="Background" Value="Red"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<i:Icon Value="{TemplateBinding Icon}" />
<TextBlock Text="{TemplateBinding Content}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Styles>
AS you can see, the content is not my control template as the icon is not visible and the background is not red. Therefore I can't use my dependencyproperty.
Any suggestions or is it not supported by the language?
I read on the documentation that is(****) for selector is intended to support derived type so I would expect my code to be working :(
Thx for help.
Try to remove IStyleable and your overriden StyleKey. You are telling the selector that it should look for a Button style and not your style.
Happy coding
Tim
PS I cannot test it on my own right now, so if it doesn't work please tell me here.
Update: I just made a blank new project and tested your code. Once I remove the StyleKey, it works:
This is the modified IconButton:
public class IconButton : Button
{
public static readonly StyledProperty<string> IconProperty =
AvaloniaProperty.Register<IconButton, string>(nameof(Icon));
public string Icon
{
get { return GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
}
If you are able to upload a minimal sample on Github, I can have a look what may be wrong.
Update 2: Looking into your demo App I see that you use a third party icon lib. The lib requires you to configure a special service at start up, see: https://github.com/Projektanker/Icons.Avalonia#1-register-icon-providers-on-app-start-up
I send you a PR which fixes your issue.
Happy coding
Tim

Error handling WPF - MVVM

I've read many articles on how to validate data in WPF - MVVM, but have gotten even more confused than I started.
What I'm trying to do is simply add a new row to my db. The validation checks if the device name (the object being inserted) is over 2 characters long AND I want to check for uniqueness, and this is where it becomes problematic.
The Model, from my understanding should take care of its own validation: e.g. character length, email address validity (if it has an email property), etc. However, what about getting access to the data layer in order to check for uniqueness? The Model should not know anything about that layer (from my understanding). This validation rule, has now become a "business" rule.
Here's my RegisterViewModel:
class RegisterViewModel : ViewModelBase, IDataErrorInfo
{
private string _DeviceName = "";
public string DeviceName
{
get { return _DeviceName; }
set
{
_DeviceName = value;
OnPropertyChanged("DeviceName");
}
}
public string Error
{
get
{
throw new NotImplementedException();
}
}
// We'd also check for uniqueness here?
public string this[string columnName]
{
get
{
string result = null;
if (columnName == "DeviceName" && String.IsNullOrEmpty(DeviceName))
{
return "Device name is required";
}
return result;
}
}
public RegisterViewModel()
{
DeviceName = System.Environment.MachineName;
}
public ICommand Register { get; set; }
}
Of course, some would argue that IDataErrorInfo should be implemented in the Model class, and I would somewhat agree, but again, the whole uniqueness problem is still there.
Here's my device Model:
public partial class device
{
public int did { get; set; }
public int uid { get; set; }
public string Name { get; set; }
}
And finally, the View
<Window x:Class="IPdevices.Register"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:IPdevices"
mc:Ignorable="d"
Title="Register" Height="202.884" Width="417.007" Icon="logo31_nowriting.ico" ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
<Grid>
<GroupBox x:Name="groupBox" Header="Device" VerticalAlignment="Top" RenderTransformOrigin="0.877,2.187" Margin="10,10,10,0" Height="96">
<Grid Margin="0,0,3,3">
<Grid.RowDefinitions>
<RowDefinition Height="47*"/>
<RowDefinition Height="49*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146*"/>
<ColumnDefinition Width="197*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="textBox" Height="24" TextWrapping="Wrap" VerticalAlignment="Top" Grid.Column="1" Margin="0,6,0,0" Text="{Binding Path=DeviceName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<Label x:Name="label" Content="Device Name:" RenderTransformOrigin="1.372,1.475" Margin="10,2,10,0" Height="28" VerticalAlignment="Top"/>
<TextBox x:Name="textBox1" Height="23" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Margin="0,7,0,0" Grid.Row="1" Grid.Column="1"/>
<Label x:Name="label1" Content="Location:" Margin="10,3,10,0" Grid.Row="1" Height="27" VerticalAlignment="Top"/>
</Grid>
</GroupBox>
<Button x:Name="button" Content="Register" Height="25" VerticalAlignment="Top" Margin="10,129,10,0" IsDefault="True" ToolTip="Register" Command="{Binding Register}"/>
</Grid>
</Window>
At this point, the validation works, and a red outline pops up. Though I'm not sure about how to deal with uniqueness property for the device name. On top of that, how do I allow the Register button become active only when everything passes?
Any input would be appreciated.
Tough question to be fair. When it comes to data validation, you can have different approaches and they are generally all working.
Thus I will give my personal view. Reading this post I wrote about MVVM may help you understand my position. So let's try to answer each of your question:
The Model, from my understanding should take care of its own
validation
Yes, as long as it is possible, but as you see you may need to do more than that for some validation
The Model should not know anything about that layer.
It's a design decision, but I would generally agree.
This validation rule, has now become a "business" rule.
I really don't understand why? Be really careful with the word "business". Is this uniqueness a true requirement from your business experts? Or is it just a technical rule because you can't store it in a relational database otherwise?
Anyway, In my opinion a less ambiguous term when you need something to validate several models, or something to validate one model but using external data (like in your example) is called a service, and it should be build around use cases (in my post I take a buying service as example)
Of course, some would argue that IDataErrorInfo should be implemented
in the Model class
And I would argue that IDataErrorInfo was designed with a really simplistic view of the world. It works in little CRUD application, but is not really convenient to manage realistic business application. I would argue that you should not use it. Just add properties in your view model, and manage the red popup and stuff by yourself. It is really not that hard, put a red border, change your property after validation, and it's done.
Though I'm not sure about how to deal with uniqueness property for the
device name.
What is your use case? Create a service around this use case, and validate the uniqueness of your device in this service.
On top of that, how do I allow the Register button become active only
when everything passes?
Forget about IDataErrorInfo, just implement it yourself. Anytime a property is updated in the UI, call your validation service again. If everytihing passes, active your button, otherwise specify to the user why it's not correct.
Does it make sense?

Displaying/styling extra info on new Windows 10 CalendarView

I've recently started development on a Windows 10 app which requires the display of events and extra information on a CalendarView. With the release of the new API, a new CalendarView component was also introduced, so I decided to give it a try. It's a nice widget, but customization has been a hell.
I've gotten to the point where I can display custom information using a ControlTemplate, but binding events and styling with VisualState has been quite a struggle.
This is the ControlTemplate I'm using wrapped in a Style.
<Style x:Key="dayItemStyle" TargetType="CalendarViewDayItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CalendarViewDayItem">
<Grid x:Name="DayItemEventListRoot">
<ListView ItemsSource="{TemplateBinding DataContext}" Padding="20,0,0,0" x:Name="EventInfoList"
IsItemClickEnabled="True" cm:Message.Attach="[Event ItemClick] = [ListTapped]">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="EventInfoPanel" Orientation="Horizontal">
<TextBlock x:Name="EventTime" Text="{Binding Date, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:HH:mm\} }"
Foreground="{Binding Foreground, RelativeSource={RelativeSource Self}}" >
</TextBlock>
<TextBlock x:Name="EventDesc" Text="{Binding Name}" Padding="5,0,0,0" Foreground="Black" >
</TextBlock>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Normal" />
<VisualState x:Name="Today" >
<VisualState.Setters>
<Setter Target="EventTime.Foreground" Value="White" />
<Setter Target="EventDesc.Foreground" Value="White" />
<Setter Target="EventTime.Text" Value="DASFASDDF" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The style is then directly set on the CalenderView component.
<CalendarView Name="FlowCalendar" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalDayItemAlignment="Top" HorizontalDayItemAlignment="Left"
Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="5" Grid.RowSpan="4" CalendarViewDayItemChanging="FlowCalendar_CalendarViewDayItemChanging"
CalendarViewDayItemStyle="{StaticResource dayItemStyle}">
</CalendarView>
Extra information and states are controlled by the CalenderViewDayItemChanging event in the code-behind.
private void FlowCalendar_CalendarViewDayItemChanging(CalendarView sender, CalendarViewDayItemChangingEventArgs args)
{
if(args.Item.Date.Date.Equals(DateTime.Now.Date))
{
VisualStateManager.GoToState(args.Item, "Today", false);
// Testing, gives NullPointerException
//TextBlock bla = (TextBlock) args.Item.FindName("EventTime");
//bla.Text = "SDADASFASDF";
}
if (args.Phase == 0)
{
var eventsByDate = ViewModel.Upcoming.FirstOrDefault(eg => eg.Key.Date == args.Item.Date.Date);
if (eventsByDate != null)
{
args.Item.DataContext = eventsByDate.ToList();
}
}
}
Setting the visual state does nothing (I've checked if it's being called), moving the VisualState(Group) outside of the ControlTemplate just gives me an error that the targets could not be found.
I'm looking to control the event listview style through visual states, which for now are custom since I'm not sure what built-in states CalendarViewDayItem has. I'm quite new to visual states, so any pointers are much appreciated.
Thanks.
Your code will not work because VisualStateManager.GoToState takes a Control as the first parameter; however, your VisualStateManager is defined inside a StackPanel of which type is Panel.
You will need to implement your own VSM which takes a Panel instead. Have a look at the answer to this post on how to do this.
However, this still won't fix your problem. As you need to somehow locate the StackPanels (note 'cause it's within a ListView, there could be multiple) and then call the ExtendedVisualStateManager.GoToState.
I would suggest you to wrap this template within a UserControl (by doing this you might do not even need to extend the VSM as you can use GoToStateAction instead) and have a dependency property (IsToday) to control the states. Then you can use ElementName binding to pass a property at the CalendarViewDayItem level down to IsToday in order to make a state change.
Update
I was actually wrong about the need of using the ExtendedVisualManager to change the visual states. Since it's already inside a UserControl, you can actually call VisualStateManager.GoToState(this, "statename", flag); directly.
However, the way you define the dependency property is wrong.
Replace the code behind with the following and it should work.
public bool IsToday
{
get { return (bool)GetValue(IsTodayProperty); }
set { SetValue(IsTodayProperty, value); }
}
public static readonly DependencyProperty IsTodayProperty =
DependencyProperty.Register("IsToday", typeof(bool), typeof(EventListTemplate), new PropertyMetadata(false, OnIsTodayChanged));
static void OnIsTodayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var template = (EventListTemplate)d;
if ((bool)e.NewValue)
{
var stateset = VisualStateManager.GoToState(template, "DayItemToday", false);
Debug.WriteLine("did it:", stateset);
}
}

Setting Textblock/Button Visibility on WP8 App using MVVM

I'm new to MVVM and I'm working on a WP8 app and I'd like to be able to set the visibility of buttons and a textblock based on when one of those buttons were tapped. Here's my View to try and explain my problem a bit better; (http://i.imgur.com/JvrxBkh.png - can't post an image on this reputation) .
When the user taps the "Going to sleep" button, I'd like the counter textblock and the "I'm awake" button to be visible with the "Going to sleep" button to be collapsed. It'll then work the other way once the "I'm awake" button is pressed, etc. If I wasn't using MVVM I'd just set the Visibility value inside the button event, but I'm stuck on how to do this when using the MVVM pattern.
I've looked around and come across a solution using a converter such as using a BooleanToVisibilityConverter class and a bool property and then setting the visibility by binding to the bool value from the ViewModel and setting the converter value for the visibility to the StaticResource BooleanToVisibilityConverter. But it just doesn't work for me the way I want. Then my counter textblock has a bind already from the ViewModel so would I need some kind of multi-binding for this textblock?
Hopefully I've explained myself OK. It seems like it should be a simple task that maybe I'm just over thinking or something.
EDIT With some code snippets
The View components that I was referring to;
<BooleanToVisibilityConverter x:Key="boolToVis" />
<TextBlock
Grid.Row="2"
Text="{Binding Counter}"
FontSize="50"
TextWrapping="Wrap"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{Binding VisibilityControl, Converter={StaticResource boolToVis}}"/>
<Button
Grid.Row="3"
Width="230"
Height="70"
Content="I'm awake"
BorderThickness="0"
Background="Gray"
Margin="0,20,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding AwakeButtonCommand}"
Visibility="{Binding VisibilityControl, Converter={StaticResource boolToVis}}""/>
<Button
Grid.Row="3"
Width="230"
Height="70"
Content="Going to sleep"
BorderThickness="0"
Background="Gray"
Margin="0,20,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding SleepButtonCommand}"
Visibility="{Binding VisibilityControl, Converter={StaticResource boolToVis}}"/>
Then in the ViewModel VisibilityControl is just;
private bool _visibilityControl;
public bool VisibilityControl
{
if (_visibilityControl != value)
_visibilityControl = value;
OnPropertyChanged("VisibilityControl");
}
And I have the two buttons such as (I'll just post one up);
public ICommand AwakeButtonCommand
{
get
{
return _awakeButtonCommand
?? (_awakeButtonCommand = new Resources.ActionCommand(() =>
{
VisibilityControl = true;
}));
}
}
It doesn't work, obviously. I think what's throwing me is because I want several things changed when one button is pressed, etc. It's throwing me off.
I've not done any Windows Phone development but here's one way of doing it in WPF that might be applicable to WP also.
First, your ViewModel would have a couple of Boolean properties indicating which state is active (one would be a mirror of the other):
public bool IsAwake
{
get
{
return _isAwake;
}
set
{
if (_isAwake != value)
{
_isAwake = value;
// raise PropertyChanged event for *both* IsAwake and IsAsleep
}
}
}
bool _isAwake;
public bool IsAsleep
{
get
{
return !_isAwake;
}
}
Then your View would contain both parts of the UI (asleep & awake) but would switch between the two parts by binding their Visibility property to these Boolean properties of your ViewModel:
<StackPanel>
<StackPanel x:Name="AwakePart"
Visibility="{Binding IsAwake, Converter={StaticResource btvc}}">
... "Going to sleep" button here ...
</StackPanel>
<StackPanel x:Name="AsleepPart"
Visibility="{Binding IsAsleep, Converter={StaticResource btvc}}">
... Elapsed time text block and "I'm awake" button here ...
</StackPanel>
</StackPanel>
You will also need a BooleanToVisibilityConverter instance somewhere in your XAML resources:
<... .Resources>
<BooleanToVisibilityConverter x:Key="btvc" />
</... .Resources>
I've used two Boolean properties in this example as it makes the XAML a little easier, however you could also use a DataTrigger -- assuming they have those in Windows Phone -- in which case you would only need one Boolean property. You would then write a trigger to toggle the Visibility properties of the two parts:
<DataTrigger Binding="{Binding IsAwake}" Value="True">
<Setter TargetName="AwakePart" Property="Visibility" Value="Visible" />
<Setter TargetName="AsleepPart" Property="Visibility" Value="Hidden" />
</DataTrigger>
For this to work you would need to explicitly set the "AwakePart" visibility to Hidden in the XAML to start with and ensure that in your ViewModel the IsAwake property is false by default. You would also need to remove the bindings on the visibility properties (as these would now be set via the trigger).

How to properly change the state of a control in WPF using data binding?

I'm very new to data binding in WPF.
Let's say I have a class called FileSource, which has one property called File (a string) and some other properties that are derived from that. In my GUI, I have a control whose appearance should change between two "modes": one mode if File is null, another mode if it's not null. Let's say that one mode sets the visibility of some child components to Visible and others to Collapsed, while the other mode does the opposite.
I can think of 3 ways to go around this:
In the FileSource, create another property of type Visibility and return the proper visibility for each control. But this to me sounds very bad - it sounds like I'll be intimately mixing the "model" (FileSource) with the behavior of the view (the control).
Create lots of trivial data conversion classes, then do data binding with a semantic property of the model (File, in this case). For example, a string -> Visibility converter for some of the components and another string -> Visibility converter (which returns the "opposite" Visibility value) for the other components. This works and plays well with the property change notifications, but creating a new class for every kind of different response I expect from the sub-controls sounds needlessly complicated to me.
Write an Update method and subscribe to the PropertyChanged event. This sounds to me like I'll be largely defeating the point of data binding.
What is the correct way to do this? Is there, perhaps, a simple way to do a data "conversion" inline in XAML (for a value I intend to read, but not write back to the source)?
You don't need too many converter classes. You need only one BoolToVisibilityConverter, but with properties which specify visibility values for true and false. You create instances like this:
<BoolToVisibilityConverter x:Key="ConvertBoolToVisible"
FalseVisibility="Collapsed" TrueVisibility="Visible" />
<BoolToVisibilityConverter x:Key="ConvertBoolToVisibleInverted"
FalseVisibility="Visible" TrueVisibility="Collapsed" />
Another converter is IsNullConverter. You can parametrize it with a property like bool InvertValue. In your resource dictionary, instances can be called ConvertIsNull and ConvertIsNotNull. Or you can create two classes if you like.
And finally, you can chain converters with ChainConverter which chains multiple value converters. You can find sample implementation in my private framework (permalink). With it, you can create converter instances in XAML like ConvertIsNotNullToVisibleInverted. Sample usage:
<a:ChainConverter x:Key="ConvertIsNotNullToVisible">
<a:ValueConverterRef Converter="{StaticResource ConvertIsNotNull}"/>
<a:ValueConverterRef Converter="{StaticResource ConvertBoolToVisible}"/>
</a:ChainConverter>
An alternative is to use triggers. XAML code will be more complex though, so I prefer converters. It requires writing some classes, but it's worth it. With architecture like this, you won't need tens of classes for every combination, and both C# and XAML code will be simple and readable.
And don't add all possible combinations of converters. Add them only when you need them. Most likely, you'll need only a few.
Consider using visual states -- these are designed for the kind of scenario you are talking about, where you have a control that needs to transition between multiple states. One advantages to using this approach over bindings, is that it allows you to use animations (including transitions).
To get it working, you declare your visual state groups, and visual states, underneath the root element of your control:
<UserControl>
<Grid x:Name="LayoutRoot">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DefaultStates">
<VisualState x:Name="State1" />
<VisualState x:Name="State2">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="textBlock2"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" To="Visible" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="textBlock1" Text="state #1" />
<TextBlock x:Name="textBlock2" Text="state #2" Visibility="Collapsed" />
</Grid>
</UserControl>
To transition between states, you can call VisualStateManager.GoToState(this, "State2", true). You can also use the Blend SDK to transition via triggers from XAML. Probably the most useful way to transition, is to use DataStateBehavior, which binds states to a view-model property:
<Grid x:Name="LayoutRoot">
<i:Interaction.Behaviors>
<ei:DataStateBehavior Binding="{Binding CurrentState}"
Value="State2"
TrueState="State2" FalseState="State1" />
</i:Interaction.Behaviors>
This way you can just update a property in your view-model, and the UI state will update automatically.
public string File
{
get { return _file; }
set
{
_file = value;
RaisePropertyChanged();
RaisePropertyChanged(() => CurrentState);
}
}
private string _file;
public string CurrentState
{
get { return (File == null ? "State1" : "State2"); }
}
Option (2) is essentially what you are going for here. You need an IValueConverter (or 2, depending on implementation).
I would call it NullToVisibilityConverter or something like that. It would return Visiblity.Visible if the value argument is not null, and Visibility.Collapsed if it is. To swap behaviors, you could just write a second converter, or utilize the ConverterParameter.
It would look like:
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(...)
{
return value != null ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(...)
{
return Binding.DoNothing;
}
}
With usage:
<Button Visibility="{Binding File, Converter={StaticResource MyConverter}"/>
And.... here is another way using styles/triggers:
MainWindow.xaml
<Window x:Class="WpfApplication19.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<StackPanel.Resources>
<Style TargetType="TextBlock" x:Key="FileIsNull">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding File}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBlock" x:Key="FileIsNotNull">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding File}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<TextBlock Text="Filename is null" Style="{StaticResource FileIsNull}" MinHeight="50" Background="Beige" />
<TextBlock Text="{Binding File}" Style="{StaticResource FileIsNotNull}" MinHeight="50" Background="Bisque" />
<Button Name="btnSetFileToNull" Click="btnSetFileToNull_Click" Content="Set File To Null" Margin="5" />
<Button Name="btnSetFileToNotNull" Click="btnSetFileToNotNull_Click" Content="Set File To Not Null" Margin="5" />
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.ComponentModel;
using System.Windows;
namespace WpfApplication19
{
public partial class MainWindow : Window
{
public FileSource fs { get; set; }
public MainWindow()
{
InitializeComponent();
fs = new FileSource();
this.DataContext = fs;
}
private void btnSetFileToNull_Click(object sender, RoutedEventArgs e)
{
fs.File = null;
}
private void btnSetFileToNotNull_Click(object sender, RoutedEventArgs e)
{
fs.File = #"C:\abc.123";
}
}
public class FileSource : INotifyPropertyChanged
{
private string _file;
public string File { get { return _file; } set { _file = value; OnPropertyChanges("File"); } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanges(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Categories