Theodoros Chatzigiannakis
Theodoros Chatzigiannakis

Reputation: 29233

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:

  1. 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).
  2. 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.
  3. 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)?

Upvotes: 3

Views: 1926

Answers (4)

J.H.
J.H.

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

McGarnagle
McGarnagle

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

Athari
Athari

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

BradleyDotNET
BradleyDotNET

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

Related Questions