7

This feels like a terribly basic question but I am sure there is a better way to do this. I have a Button in my UI which selects a specific tab and fire a Command from the ViewModel

Here is the current code (which works fine):

XAML:

<Button Content="Button!" Click="OnButtonClick" Command="{Binding WhateverCommand}" />

Code behind:

private void OnButtonClick(object sender, RoutedEventArgs e)
{
     theTab.IsSelected = true;
}

Isn't there any cleaner, XAML-only way to do that UI operation? I was thinking about something like:

<Button Content="Button!" Click="OnButtonClick" Command="{Binding WhateverCommand}">
    <Button.Triggers>
        <EventTrigger RoutedEvent="Click">
            <Setter TargetName="theTab" Property="IsSelected" Value="True" />
        </EventTrigger>
    </Button.Trigger>
</Button>

But unfortunately it seems like the EventTrigger won't support a Click event. Why so? I am still sometimes confused with triggers after a few years working in WPF, and this pretty much sums it up. When trying to build that I have an error on the Setter line:

A value of type 'Setter' cannot be added to a collection or dictionary of type 'TriggerActionCollection'.

Thank you!

EDIT since I was ask the XAML structure of my Window, it looks like this:

<DockPanel LastChildFill="True">
    <Ribbon DockPanel.Dock="Top">
        <Button Content="Button!" Click="OnButtonClick" Command="{Binding WhateverCommand}" />
    </Ribbon>
    <TabControl>
        <TabItem x:Name="theTab" />
    </TabControl>
</DockPanel>
Damascus
  • 6,553
  • 5
  • 39
  • 53
  • can you post your complete xaml including the tab you are talking about? – Nitin Jun 03 '14 at 09:45
  • I believe [this](http://stackoverflow.com/questions/942548/setting-a-property-with-an-eventtrigger) and/or [this](http://stackoverflow.com/questions/3870214/eventtrigger-with-setter-in-wpf) might be an answer to you... (And also use Button.Click instead of Click) – qqbenq Jun 03 '14 at 09:46
  • It is not relevant to the question itself imo. Do you need it? – Damascus Jun 03 '14 at 09:47
  • yes. I want to see the hierarchy where button is and where tab is? – Nitin Jun 03 '14 at 09:49
  • @qqbenq : Wow, this seems unexpectedly... complicated for a simple tab change. Is there any other way than a `storyboard` for achieving such a simple thing? I believe it's actually clearer to use a code-behind handler rather than an animation for this @nit : updated with a basic XAML skeleton – Damascus Jun 03 '14 at 09:53
  • 1
    @Damascus I agree, it seems awfully complicated, but this is how it can be done :) You can create a storyboard resource, and use it in the Trigger, it will look a little bit simpler. – qqbenq Jun 03 '14 at 09:57
  • Indeed. I'll look into a way to simplify that. Maybe create a specific Storyboard class designed to set a property. Ugh, sometimes WPF just amazes me by how simple stuff can be complicated! – Damascus Jun 03 '14 at 10:05

3 Answers3

9

Error sums it up. You cannot use Setter in EventTrigger. If you want to do it in XAML you can use Storyboard that will select given tab when button is pressed. Something like this:

<Button Content="Click">
   <Button.Triggers>
       <EventTrigger RoutedEvent="Button.Click">
           <BeginStoryboard>
               <Storyboard>
                   <BooleanAnimationUsingKeyFrames Storyboard.TargetName="theTab" Storyboard.TargetProperty="IsSelected">
                       <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
                   </BooleanAnimationUsingKeyFrames>
               </Storyboard>
           </BeginStoryboard>
       </EventTrigger>
   </Button.Triggers>
</Button>
dkozl
  • 32,814
  • 8
  • 87
  • 89
  • Somebody also posted this as a comment, and my concern is: for the sake of simplicity, why would a `Storyboard` with an `Animation` be more simple/clean than a code-behind `EventHandler` ? If this is the only way to do this, I think I'll probably stick to the code-behind way. I'll keep the question open to see if anything else comes up before accepting your answer :) Thank you! – Damascus Jun 03 '14 at 09:56
  • You're right, in this case, code behind is simpler. The problem is that normal `Trigger` has concept of state so `Setter` can be undone but `EventTrigger` has no such concept so all you can do is start some actions when it happens. – dkozl Jun 03 '14 at 10:07
  • Oh, so that's why. I'm always confused between all of these `Triggers` possibility. why would those in a `Style` be different! I'll try to work a shorter solution. Maybe by creating a separate `Storyboard` derived class – Damascus Jun 03 '14 at 10:11
4

You can introduce a bool property IsMyTabSelected to your VM, bind it to TabItem.IsSelected:

<TabItem x:Name="theTab" IsSelected="{Binding IsMyTabSelected}" >

Then you just set this flag in the WhateverCommand handler for the Button. Note: The IsMyTabSelected property must implement System.ComponentModel.INotifyPropertyChanged.

amnezjak
  • 2,011
  • 1
  • 14
  • 18
  • Then it's not XAML-Only! Also no real point in keeping a `IsMyTabSelected` pure UI property in my ViewModel since that can just be switched in XAML code behind. What's fun is that for such a simple problem, every single solution seems equally not adapted :/ – Damascus Jun 03 '14 at 10:08
  • 1
    Not XAML-only, but also no code-behind and simple. Moreover, it is tough (impossible?) to keep `ViewModel` away from 'pure UI' in large project. – amnezjak Jun 03 '14 at 10:12
4

There definitely is a better way. With the help of the Windows.Interactivity assembly you are able to bind the event source to a singe class, containing only the associated action. With this you can almost do everything you ned.

The action class has to derive from TriggerAction. By overriding the Invoke-method you can specify the action.

Despite this scenario it also possible to bind the EventTrigger to a command (e.g. relay command), allowing a clean MMVM implementation.

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

<Button x:Name="button">
                        <i:Interaction.Triggers>
                            <i:EventTrigger SourceName="button" EventName="Click">
                                <app:MyAction/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
Public Class MyAction
    Inherits Interactivity.TriggerAction(Of UIElement)

    Protected Overrides Sub Invoke(parameter As Object)
        MsgBox("Clicked")
    End Sub

End Class

I updated the code to meet your specific requirements. The TriggerAction class now also contains a dependency property, which can be cound to your tab control:

<TabControl x:Name="tab"/>
                    <Button x:Name="button">
                        <i:Interaction.Triggers>
                            <i:EventTrigger SourceName="button" EventName="Click">
                                <app:MyAction Target="{Binding ElementName=tab}"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
Public Class MyAction
    Inherits Interactivity.TriggerAction(Of UIElement)

    Protected Overrides Sub Invoke(parameter As Object)
        DirectCast(Target, TabControl).SelectedIndex = 0
    End Sub

    Shared Sub New()
        _targetProperty = DependencyProperty.Register(
          "Target",
          GetType(UIElement),
          GetType(MyAction),
          New UIPropertyMetadata(Nothing))
    End Sub

    Private Shared _targetProperty As DependencyProperty
    Public Shared ReadOnly Property TargetProperty As DependencyProperty
        Get
            Return _targetProperty
        End Get
    End Property

    Property Target As UIElement
        Get
            Return DirectCast(GetValue(TargetProperty), UIElement)
        End Get
        Set(value As UIElement)
            SetValue(TargetProperty, value)
        End Set
    End Property

End Class
O'Neil
  • 3,790
  • 4
  • 16
  • 30
Dennis Kassel
  • 2,726
  • 4
  • 19
  • 30