如何在XAML中将选择更改事件绑定到其他元素的可见性属性上

3
给出一个ComboBox,选择后应该变成一个可见的TextBlock。我使用ViewModel构建了这个功能。
视图:
<ComboBox SelectionChanged="{mvvmHelper:EventBinding OnSelectionChanged}" />
<TextBlock Visibility="{Binding LanguageChanged, Converter={StaticResource BooleanVisibilityConverter}}"/>

ViewModel:

bool LanguageChanged = false;

void OnSelectionChanged() => LanguageChanged = true;

我希望您能提供一种优雅的解决方案,仅使用XAML完成

目前为止我尝试过的:

<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>

我想我得使用Storyboard

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

另一个选择是 System.Windows.Interactivity,但这在 WpfCore 3.1 中不可用。
2个回答

4
你有几个不错的选择。
由于最后一个解决方案使用了一个 DataTrigger,因为它允许在 ComboBox.SelectedItem 的某些状态下触发,所以我建议您实现它来解决问题。这也是一种只需要XAML就能解决的方案,不需要像 LanguageChanged 这样的额外属性。

动画触发属性

为了动画化像 LanguageChanged 这样的属性,该属性必须是一个 DependencyProperty。因此,第一个示例将 LanguageChanged 实现为 MainWindow 的一个 DependencyProperty

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>

直接对目标控件进行动画

如果您希望切换可见性的控件与触发控件在同一个范围内,您可以直接对Visibility属性进行动画:

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>

实现 IValueConverter 接口

如果您想要为触发器添加更多条件,比如选中哪个值,您可以将 TextBlock.Visibility 绑定到 ComboBox.SelectedItem 上,并使用 IValueConverter 来决定是返回 Visibility.Visible 还是 Visibility.Hidden,具体基于当前所选项:

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>

实现TextBlock上的DataTrigger

如果您希望向触发器添加更多条件,例如选择了哪个值,您还可以在上添加,该会在的一个或多个属性上触发。然后,您必须将强制转换为底层项的实际类型,以便在绑定路径中引用该项的属性。
以下示例将转换为虚构类型,以访问属性,以在特定选定语言上触发:

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>

我已经实现了IValueConverter转换器。易于重用并且对其他人来说易读。 - LWS
2
不错的选择。我想指出,定义一组DataTriggersStyle也是可重用和易读的。您可以使用Style.BasedOn属性轻松扩展(“继承”)样式。如果您需要处理多个条件,则MultiDataTrigger(或MultiTrigger)也是一个好的灵活解决方案。当然,在特殊情况下,IValueConverter更加强大,例如,如果您需要访问模板或使用反射。 - BionicCode

0

我觉得@BionicCode已经给出了相当全面的答案,但我想再补充一点。

我认为最符合您要求的最佳解决方案是样式触发器。
我看到Bionic已经包括了这个,但这里有一个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>

然而... 如果你真的在应用程序中进行本地化,而不仅仅是使用这个作为例子,那么我认为有更好的解决方案。
首先花几分钟阅读关于WPF全球化和本地化的内容。

然后在项目属性中添加至少一个语言资源文件(例如'Resources.ja-JP.resx'),并不要忘记将你的Resources.resx文件标记为public。在这些.resx文件中放置一些本地化字符串。

然后将你的TextBlock的文本绑定到该属性:

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

接下来,您需要一些代码来处理切换文化的问题。这里有很多选项,但我会包含一些我过去使用过的代码。

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);
        }

    }
}

最后一个问题应该是如何从xaml提供对文化资源的访问方式。这可以使用ObjectDataProvider来完成。

您可以直接将其放在App.xaml中或放在单独的文件中,并在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>

当您这样做时,您的字符串绑定可以自动匹配系统文化(否则将成为通用资源中的默认值)。此外,用户可以随时切换文化。

在您的示例中,ComboBox SelectionChanged事件将被用作更改文化的起点,如下所示:

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

我更喜欢使用命令来完成这个任务,但这取决于你。


绑定"ElementName=LanguageCB, Path=SelectedIndex}" Value="1"将不起作用。您不知道初始索引。为使其工作,您可以做一个自定义顺序,上次选择总是索引0。 - LWS
@LWS 你是说 LanguageCB 的 ItemSource 是动态的吗?只要你知道 CB 中的每个项目,你就可以为每个项目添加一个 DataTrigger。或者你可以默认让 Textbox 可见,并且只有一个 DataTrigger,当选择“None”项目时将其隐藏。 - Scott Solmer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接