How to bind SelectionChanged Event to Visibility Property on other Element in XAML Only

Viewed 651

Given is an ComboBox after SelectionChanged should become an TextBlock visible. I build this functionality using ViemModel.

View:

<ComboBox SelectionChanged="{mvvmHelper:EventBinding OnSelectionChanged}" />
<TextBlock Visibility="{Binding LanguageChanged, Converter={StaticResource BooleanVisibilityConverter}}"/>

ViewModel:

bool LanguageChanged = false;

void OnSelectionChanged() => LanguageChanged = true;

I looking for a elegant solution done in XAML only

What i try so far:

<Style TargetType="{x:Type TextBlock}">
    <Setter Property="Visibility" Value="Collapsed" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding IsDropDownOpen, ElementName=Box, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Value="True">
            <Setter Property="Visibility" Value="Visible" />
        </DataTrigger>
    </Style.Triggers>
</Style>

I guess I have to use Storyboard

<ComboBox.Style>
    <Style TargetType="{x:Type ComboBox}">
        <Style.Triggers>
            <EventTrigger RoutedEvent="SelectionChanged">
                <BeginStoryboard>
                    <Storyboard>
                        ???
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Style.Triggers>
    </Style>
</ComboBox.Style>

the other option would be System.Windows.Interactivity but this is not available in WpfCore 3.1

2 Answers

You have several good options here.
Since the last solution, which uses a DataTrigger, is the most flexible, as it allows to trigger on certain states of the ComboBox.SelectedItem, I recommend to implement it to solve your problem. It's also a XAML only solution and doesn't require an extra property like LanguageChanged.

Animate a trigger property

In order to animate a property like LanguageChanged, the property must be a DependencyProperty. The first example therefore implements LanguageChanged as a DependencyProperty of MainWindow:

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public static readonly DependencyProperty LanguageChangedProperty = DependencyProperty.Register(
    "LanguageChanged",
    typeof(bool),
    typeof(MainWindow),
    new PropertyMetadata(default(bool)));

  public bool LanguageChanged
  {
    get => (bool) GetValue(MainWindow.LanguageChangedProperty);
    set => SetValue(MainWindow.LanguageChangedProperty, value);
  }
}

MainWindow.xaml

<Window x:Name="Window">
  <StackPanel>

    <TextBlock Text="Invisible"
               Visibility="{Binding RelativeSource={RelativeSource AncestorType=MainWindow}, Path=LanguageChanged, Converter={StaticResource BooleanToVisibilityConverter}}" />

    <ComboBox>
      <ComboBox.Triggers>
        <EventTrigger RoutedEvent="ComboBox.SelectionChanged">
          <BeginStoryboard>
            <Storyboard>
              <BooleanAnimationUsingKeyFrames Storyboard.TargetName="Window"
                                              Storyboard.TargetProperty="LanguageChanged">
                <DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
              </BooleanAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </ComboBox.Triggers>
    </ComboBox>
  </StackPanel>
</Window>

Animate the target control directly

If the control you wish to toggle the visibility of is in the same scope as the triggering control you can animate the Visibility directly:

MainWindow.xaml

<Window x:Name="Window">
  <StackPanel>

    <TextBlock x:Name="InvisibleTextBlock"
               Text="Invisible"
               Visibility="Hidden" />

    <ComboBox>
      <ComboBox.Triggers>
        <EventTrigger RoutedEvent="ComboBox.SelectionChanged">
          <BeginStoryboard>
            <Storyboard>
              <ObjectAnimationUsingKeyFrames Storyboard.TargetName="InvisibleTextBlock"
                                             Storyboard.TargetProperty="Visibility">
                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
              </BooleanAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </ComboBox.Triggers>
    </ComboBox>
  </StackPanel>
</Window>

Implement IValueConverter

In case you wish to add more conditions to the trigger like which value was selected, you should bind the TextBlock.Visibility to ComboBox.SelectedItem and use a IValueConverter to decide whether to return Visibility.Visible or Visibilty.Hidden based on the current selected item:

MainWindow.xaml

<Window x:Name="Window">
  <Window.Resources>

    <!-- TODO::Implement IValueConverter -->
    <SelectedItemToVisibilityConverter x:Key="SelectedItemToVisibilityConverter" />
  </Window.Resources>

  <StackPanel>

    <TextBlock Text="Invisible"
               Visibility="{Binding ElementName=LanguageSelector, Path=SelectedItem, Converter={StaticResource SelectedItemToVisibilityConverter}}" />

    <ComboBox x:Name="LanguageSelector" />
  </StackPanel>
</Window>

Implement a DataTrigger on the TextBlock

In case you wish to add more conditions to the trigger like which value was selected, you could also add a DataTrigger to the TetxtBlock which triggers on one or more properties of the ComboBox.SelectedItem. You then have to cast the SelectedItem to the actual type of the underlying ComboBox items in order to reference the item's properties in the binding path.
The following example casts the SelectedItem to an imaginary type LanguageItem to access the LanguageItem.LanguageName property, to trigger on a specific selected language:

MainWindow.xaml

<Window x:Name="Window">
  <StackPanel>

    <TextBlock x:Name="InvisibleTextBlock" Text="Invisible">
      <TextBlock.Style>
        <Style TargetType="TextBlock">
          <Setter Property="Visibility" Value="Hidden"/>
          <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=LanguageSelector, Path=SelectedItem.(LanguageItem.LanguageName)}" 
                         Value="English">
              <Setter Property="Visibility" Value="Visible"/>
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </TextBlock.Style>
    </TextBlock>

    <ComboBox x:Name="LanguageSelector" />
  </StackPanel>
</Window>

I feel like @BionicCode has given quite the comprehensive answer, but I'll add my 2¢.

The best solution that meets your requirements I think is the style trigger.
I see that Bionic included that, but here is an MCVE:

<Window x:Class="project-name.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="120" Width="300">
    <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="15,15,0,0">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Language:   " VerticalAlignment="Center"/>
            <ComboBox x:Name="LanguageCB" HorizontalAlignment="Left" SelectedIndex="0">
                <ComboBoxItem Content="None ?"/>
                <ComboBoxItem Content="English"/>
            </ComboBox>
        </StackPanel>
        <Border Margin="0,10,0,0" BorderThickness="1" BorderBrush="Black" Padding="2">
            <TextBlock Text="Becomes visible when &quot;LanguageCB&quot; changes selection">
                <TextBlock.Style>
                    <Style TargetType="{x:Type TextBlock}">
                        <Setter Property="Visibility" Value="Hidden"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ElementName=LanguageCB, Path=SelectedIndex}" Value="1">
                                <Setter Property="Visibility" Value="Visible"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </Border>
    </StackPanel>
</Window>

However... If you are really doing localization in your application and not just using this as an example, then I think there is a better solution.
First take a few minutes to read about WPF Globalization and Localization.

Then add at least 1 language resource file to your project's properties (for example 'Resources.ja-JP.resx', and don't forget to mark your Resources.resx file to public. Put some localized strings in these .resx files.

Then bind your TextBlock's text to the property:

<TextBlock Text="{Binding Path=ResourceName, Source={StaticResource Resources}}"/>

Next you need some code to handle switching cultures. There a lots of options here, but I'll include some code I've used in the past.

CultureResources.cs

namespace Multi_Language_Base_App.Cultures
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Diagnostics;
    using System.Windows.Data;

    /// <summary>
    /// Wraps up XAML access to instance of Properties.Resources, 
    /// list of available cultures, and method to change culture </summary>
    public class CultureResources
    {
        private static ObjectDataProvider provider;

        public static event EventHandler<EventArgs> CultureUpdateEvent;

        //only fetch installed cultures once
        private static bool bFoundInstalledCultures = false;

        private static List<CultureInfo> pSupportedCultures = new List<CultureInfo>();
        /// <summary>
        /// List of available cultures, enumerated at startup
        /// </summary>
        public static List<CultureInfo> SupportedCultures
        {
            get { return pSupportedCultures; }
        }

        public CultureResources()
        {
            if (!bFoundInstalledCultures)
            {
                //determine which cultures are available to this application
                Debug.WriteLine("Get Installed cultures:");
                CultureInfo tCulture = new CultureInfo("");


                foreach (string dir in Directory.GetDirectories(AppDomain.CurrentDomain.BaseDirectory))
                {
                    try
                    {
                        //see if this directory corresponds to a valid culture name
                        DirectoryInfo dirinfo = new DirectoryInfo(dir);
                        tCulture = CultureInfo.GetCultureInfo(dirinfo.Name);

                        //determine if a resources dll exists in this directory that matches the executable name
                        string exe = System.Reflection.Assembly.GetExecutingAssembly().Location;

                        if (dirinfo.GetFiles(Path.GetFileNameWithoutExtension(exe) + ".resources.dll").Length > 0)
                        {
                            pSupportedCultures.Add(tCulture);
                            Debug.WriteLine(string.Format(" Found Culture: {0} [{1}]", tCulture.DisplayName, tCulture.Name));
                        }
                    }
                    catch (ArgumentException) //ignore exceptions generated for any unrelated directories in the bin folder
                    {
                    }
                }
                bFoundInstalledCultures = true;
            }
        }

        /// <summary>
        /// The Resources ObjectDataProvider uses this method to get 
        /// an instance of the _This Application Namespace_.Properties.Resources class
        /// </summary>
        public Properties.Resources GetResourceInstance()
        {
            return new Properties.Resources();
        }


        public static ObjectDataProvider ResourceProvider
        {
            get
            {
                if (provider == null)
                    provider = (ObjectDataProvider)App.Current.FindResource("Resources");
                return provider;
            }
        }

        /// <summary>
        /// Change the current culture used in the application.
        /// If the desired culture is available all localized elements are updated.
        /// </summary>
        /// <param name="culture">Culture to change to</param>
        public static void ChangeCulture(CultureInfo culture)
        {
            // Remain on the current culture if the desired culture cannot be found
            // - otherwise it would revert to the default resources set, which may or may not be desired.
            if (pSupportedCultures.Contains(culture))
            {
                Properties.Resources.Culture = culture;
                ResourceProvider.Refresh();

                RaiseCultureUpdateEvent(null, new EventArgs());

                Debug.WriteLine(string.Format("Culture changed to [{0}].", culture.NativeName));
            }
            else
            {
                Debug.WriteLine(string.Format("Culture [{0}] not available", culture));
            }
        }

        private static void RaiseCultureUpdateEvent(object sender, EventArgs e)
        {
            EventHandler<EventArgs> handleit = CultureUpdateEvent;
            CultureUpdateEvent?.Invoke(sender, e);
        }

    }
}

The last piece of the puzzle should be a way to provide access to the culture resources from xaml. This is done using an ObjectDataProvider.

You can put this directly in App.xaml or a separate file and reference it in App.xaml.

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Cultures="clr-namespace:Multi_Language_Base_App.Cultures">
    <!-- Contains the current instance of the ProjectName.Properties.Resources class.
         Used in bindings to get localized strings and automatic updates when the culture is updated -->
    <ObjectDataProvider x:Key="Resources" 
                        ObjectType="{x:Type Cultures:CultureResources}" 
                        MethodName="GetResourceInstance"/>

    <!-- Provides access to list of currently available cultures -->
    <ObjectDataProvider x:Key="CultureResourcesDS" 
                        ObjectType="{x:Type Cultures:CultureResources}"/>

</ResourceDictionary>

When you do this, your string bindings can automatically match the system culture from the get-go (or else end up as the defaults in your generic resources). Also the user can switch the culture on the fly.

In your example, the ComboBox SelectionChanged event would be used as a starting point to change the culture like this:

CultureInfo CultureJapanese = new CultureInfo("ja-JP");
Cultures.CultureResources.ChangeCulture(CultureJapanese);

I prefer to use commanding to get this done, but it's up to you.

Related