简体   繁体   中英

C# WPF - How to Combine datatrigger and trigger?

I don't know if need to combine DataTrigger & Trigger, if there's better way please tell me.

My goal is, to create a menu(with icons), icons will change while meet hover or selected event.

Here's an enum define all menu types:

public enum PageTypes:byte
{
    NotSet = 0,
    HomePage = 1,
    ShopPage = 2,
    AboutPage = 3
}

Then I created a MenuItemModel represent each menu item:

public class MenuItemModel : INotifyPropertyChanged
{
    private PageTypes _menuItemType = PageTypes.NotSet;
    public PageTypes MenuItemType { get { return _menuItemType; } set { if (value != _menuItemType) { _menuItemType = value; RaisePropertyChanged(() => MenuItemType); } } }

    private bool _isSelected = false;
    public bool IsSelected { get { return _isSelected; } set { if (value != _isSelected) { _isSelected = value; RaisePropertyChanged(() => IsSelected); } } }
}

Ok, then I begin to create UI.

<!-- MenuItem Template -->
<DataTemplate x:Key="MenuTemplate">
    <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
        <Image>
            <Image.Style>
                <Style TargetType="Image">
                    <Setter Property="Source" Value="/Image/Home_normal.png"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
                            <Setter Property="Source" Value="/Image/Shop_normal.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="AboutPage">
                            <Setter Property="Source" Value="/Image/About_normal.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>
    </Button>
</DataTemplate>

till now everything is very easy, but when I try to make mouseOver and Selected effect, problem comes.

for example, if mouse over home_normal.png, it should change to home_hover.png, if IsSelected property is TRUE, image should be ignore hover trigger then use home_selected.png. But there's 3 image, how do I know what image should change?

<!-- MenuItem Template -->
<DataTemplate x:Key="MenuTemplate">
    <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
        <Image>
            <Image.Style>
                <Style TargetType="Image">
                    <Setter Property="Source" Value="/Image/Home_normal.png"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
                            <Setter Property="Source" Value="/Image/Shop_normal.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding MenuItemType}" Value="AboutPage">
                            <Setter Property="Source" Value="/Image/About_normal.png"/>
                        </DataTrigger>

                        <!-- MY PLAN -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Source" Value="?_hover.png"/>
                        </Trigger>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter Property="Source" Value="?_selected.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>
    </Button>
</DataTemplate>

If you can see the question mark in "MY PLAN" comment, that would be my question: what should I do in the Value field?

You can use MultiDataTrigger like this. But you should add same 3 triggers for all types of pages. Note that next trigger overrides below and conditions works like logical AND.

<p:Style.Triggers xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <DataTrigger Binding="{Binding MenuItemType}" Value="ShopPage">
        <Setter Property="Source" Value="/Image/Shop_normal.png"/>
    </DataTrigger>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding MenuItemType}" Value="ShopPage" />
            <Condition Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsMouseOver}" Value="true" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Source" Value="/Image/Shop_MouseOver.png" />
    </MultiDataTrigger>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding MenuItemType}" Value="ShopPage" />
            <Condition Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsSelected}" Value="true" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Source" Value="/Image/Shop_IsSelected.png" />
    </MultiDataTrigger>
</p:Style.Triggers>

In my opinion, the answer you've already received and accepted is a good one. It's entirely XAML-based, which seems to be a primary goal in your scenario, and it should work very well. That said, the XAML-only solution is fairly verbose and involves a lot of redundant code. This is already seen in the scenario above where you have two buttons types, each with three possible states. And it will only get worse as you add button types and states.

If you are willing to do a little code-behind, I think you can accomplish the same effect but with a lot less redundancy.

Specifically, if you use <MultiBinding> , you can bind the relevant properties to a collection that can be used to look up the correct image source. In order for me to accomplish this, I needed to create a couple of small container types to store the lookup data, and of course the IMultiValueConverter implementation to use them:

Container types:

[ContentProperty("Elements")]
class BitmapImageArray
{
    private readonly List<ButtonImageStates> _elements = new List<ButtonImageStates>();

    public List<ButtonImageStates> Elements
    {
        get { return _elements; }
    }
}

class ButtonImageStates
{
    public string Key { get; set; }
    public BitmapImage[] StateImages { get; set; }
}

Converter:

class OrderedFlagConverter : IMultiValueConverter
{
    public object Convert(object[] values,
        Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        BitmapImageArray imageData = (BitmapImageArray)parameter;
        string type = (string)values[0];

        foreach (ButtonImageStates buttonStates in imageData.Elements)
        {
            if (buttonStates.Key == type)
            {
                int index = 1;

                while (index < values.Length)
                {
                    if ((bool)values[index])
                    {
                        break;
                    }

                    index++;
                }

                return buttonStates.StateImages[index - 1];
            }
        }

        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value,
        Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

In your example, using the above might look something like this:

<DataTemplate x:Key="MenuTemplate">
  <Button Command="{Binding ClickCommand}" CommandParameter="{Binding}">
    <Image>
      <Image.Source>
        <MultiBinding>
          <MultiBinding.Converter>
            <l:OrderedFlagConverter/>
          </MultiBinding.Converter>
          <MultiBinding.ConverterParameter>
            <l:BitmapImageArray>
              <l:ButtonImageStates Key="ShopPage">
                <l:ButtonImageStates.StateImages>
                  <x:Array Type="{x:Type BitmapImage}">
                    <BitmapImage UriSource="/Image/Shop_selected.png"/>
                    <BitmapImage UriSource="/Image/Shop_hover.png"/>
                    <BitmapImage UriSource="/Image/Shop_normal.png"/>
                  </x:Array>
                </l:ButtonImageStates.StateImages>
              </l:ButtonImageStates>
              <l:ButtonImageStates Key="AboutPage">
                <l:ButtonImageStates.StateImages>
                  <x:Array Type="{x:Type BitmapImage}">
                    <BitmapImage UriSource="/Image/About_selected.png"/>
                    <BitmapImage UriSource="/Image/About_hover.png"/>
                    <BitmapImage UriSource="/Image/About_normal.png"/>
                  </x:Array>
                </l:ButtonImageStates.StateImages>
              </l:ButtonImageStates>
            </l:BitmapImageArray>
          </MultiBinding.ConverterParameter>
          <Binding Path="ButtonType"/>
          <Binding Path="IsMouseOver" RelativeSource="{RelativeSource Self}"/>
          <Binding Path="IsSelected"/>
        </MultiBinding>
      </Image.Source>
    </Image>
  </Button>
</DataTemplate>

The converter takes, as input, bindings to the properties that affect the visual state of the button. The first bound value is simply the type of the button; this is used to look up the correct array of button states for the button. The remaining bound values (you can have arbitrarily many in this approach) are flags that are searched; the images are stored in the same order as the flags, with one additional "default" image at the end (ie if no flags are set, the default image is returned).

In this way, adding new button types involves only adding a new ButtonImageStates object, specifying the correct key for that button type, and adding new button states involves only adding a single line to each button type's list: the BitmapImage reference that corresponds to the image for that state for that button type.

Doing it this way drastically cuts down on the amount of code one has to add as new button types and states are needed: a given button type need be mentioned in the XAML only once, and likewise each triggering property is mentioned only once. A XAML-only approach will require a lot of duplicated boilerplate, and the actual image file references will be scattered throughout the style declaration.


Here is a simple demo of the basic technique. Lacking a good MCVE to start with, I didn't want to waste time re-creating parts of the code that weren't strictly necessary for the purposes of a demonstration:

  • I only bothered to create four state images, and of course only wrote code to deal with four possible states: two each for two different button types.
  • I also didn't bother with putting this in a menu; I'm just using a plain ItemsControl to present the buttons.
  • Naturally, the view model is a degenerate class; I didn't bother with property-change notification, since it's not needed here. The example still works if you include that though.

Here are the images used in the example (I'm a programmer, not an artist…I considered not even bothering with image content, since that's also not strictly required to demonstrate the basic technique, but figured I could handle four basic images :) ):

red_normal.png
red_hover.png
green_normal.png
green_hover.png

These are added to the project in a "Resources" folder, with the Build Action set to Resource .

XAML:

<Window x:Class="TestSO34193266MultiTriggerBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:TestSO34193266MultiTriggerBinding"
        Title="MainWindow" Height="350" Width="525">

  <Window.Resources>
    <l:OrderedFlagConverter x:Key="orderedFlagConverter1"/>

    <BitmapImage x:Key="bitmapRedNormal"
                 UriSource="pack://application:,,,/Resources/red_normal.png"/>
    <BitmapImage x:Key="bitmapRedHover"
                 UriSource="pack://application:,,,/Resources/red_hover.png"/>
    <BitmapImage x:Key="bitmapGreenNormal"
                 UriSource="pack://application:,,,/Resources/green_normal.png"/>
    <BitmapImage x:Key="bitmapGreenHover"
                 UriSource="pack://application:,,,/Resources/green_hover.png"/>

    <l:ViewModel x:Key="redViewModel" ButtonType="Red"/>
    <l:ViewModel x:Key="greenViewModel" ButtonType="Green"/>

    <x:Array x:Key="items" Type="{x:Type l:ViewModel}">
      <StaticResource ResourceKey="redViewModel"/>
      <StaticResource ResourceKey="greenViewModel"/>
    </x:Array>

    <x:Array x:Key="redButtonStates" Type="{x:Type BitmapImage}">
      <StaticResource ResourceKey="bitmapRedHover"/>
      <StaticResource ResourceKey="bitmapRedNormal"/>
    </x:Array>

    <x:Array x:Key="greenButtonStates" Type="{x:Type BitmapImage}">
      <StaticResource ResourceKey="bitmapGreenHover"/>
      <StaticResource ResourceKey="bitmapGreenNormal"/>
    </x:Array>

    <l:BitmapImageArray x:Key="allButtonStates">
      <l:ButtonImageStates Key="Red" StateImages="{StaticResource redButtonStates}"/>
      <l:ButtonImageStates Key="Green" StateImages="{StaticResource greenButtonStates}"/>
    </l:BitmapImageArray>

    <ItemsPanelTemplate x:Key="panelTemplate">
      <StackPanel IsItemsHost="True" Orientation="Horizontal"/>
    </ItemsPanelTemplate>

    <DataTemplate x:Key="template" DataType="l:ViewModel">
      <Button>
        <Image Stretch="None">
          <Image.Source>
            <MultiBinding Converter="{StaticResource orderedFlagConverter1}"
                          ConverterParameter="{StaticResource allButtonStates}">
              <Binding Path="ButtonType"/>
              <Binding Path="IsMouseOver" RelativeSource="{RelativeSource Self}"/>
            </MultiBinding>
          </Image.Source>
        </Image>
      </Button>
    </DataTemplate>

    <!-- explicit namespace only for the benefit of Stack Overflow formatting -->
    <p:Style TargetType="ItemsControl"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
      <Setter Property="ItemsSource" Value="{StaticResource items}"/>
      <Setter Property="ItemsPanel" Value="{StaticResource panelTemplate}"/>
    </p:Style>
  </Window.Resources>

  <StackPanel>
    <ItemsControl ItemTemplate="{StaticResource template}"/>
  </StackPanel>
</Window>

C#:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

class ViewModel
{
    public string ButtonType { get; set; }
}

class OrderedFlagConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        BitmapImageArray imageData = (BitmapImageArray)parameter;
        string type = (string)values[0];

        foreach (ButtonImageStates buttonStates in imageData.Elements)
        {
            if (buttonStates.Key == type)
            {
                int index = 1;

                while (index < values.Length)
                {
                    if ((bool)values[index])
                    {
                        break;
                    }

                    index++;
                }

                return buttonStates.StateImages[index - 1];
            }
        }

        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

[ContentProperty("Elements")]
class BitmapImageArray
{
    private readonly List<ButtonImageStates> _elements = new List<ButtonImageStates>();

    public List<ButtonImageStates> Elements
    {
        get { return _elements; }
    }
}

class ButtonImageStates
{
    public string Key { get; set; }
    public BitmapImage[] StateImages { get; set; }
}

One minor note: for some reason I get in the XAML editor the following error message on the <Window> element declaration:

Collection property 'TestSO34193266MultiTriggerBinding.ButtonImageStates'.'StateImages' is null.

I've clearly failed to jump through some hoop the XAML editor wants me to clear with respect to the declaration and/or implementation of ButtonImageStates , but what that is I don't know. The code compiles and runs just fine, so I haven't bothered to try to figure that part out. It may well be the case that there's a better way to represent the map of button state images, but this way works and other than the spurious error seems fine to me.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM