Reputation: 29233
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:
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).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.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)?
Upvotes: 3
Views: 1926
Reputation: 4322
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));
}
}
}
Upvotes: 1
Reputation: 102793
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"); }
}
Upvotes: 2
Reputation: 34293
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.
Upvotes: 3
Reputation: 61379
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}"/>
Upvotes: 1