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>
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>
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
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.
Related
I solved the problem in such a way that there is an EventHandler in the Code-Behind which then calls the corresponding Command of the ViewModel.
<Storyboard x:Key="StartNewGameAnimation" RepeatBehavior="1x"
Completed="StartAnimationCompleted">
private void StartAnimationCompleted(object sender, EventArgs e)
{
var gameBoardViewModel = (GameBoardViewModel)this.DataContext;
gameBoardViewModel.StartGameWhenStartanimationCompletedCommand.Execute(null);
}
This solution works fine, but I want to handle the Event directly in Xaml and also call the Command. It is simply important to me whether this path exists.
You can use the Microsoft.Xaml.Behaviors.Wpf NuGet package that replaces the legacy Interactivity types. There is an InvokeCommandAction type that can invoke a command through an EventTrigger.
However, the direct way inside Storyboard does not work.
<Storyboard x:Key="StartNewGameAnimation" RepeatBehavior="1x">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Completed">
<b:InvokeCommandAction Command="{Binding StartGameWhenStartanimationCompletedCommand}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
</Storyboard>
It will throw an exception at runtime, because the Storyboard cannot be frozen with the binding.
'Microsoft.Xaml.Behaviors.Interactivity.EventTrigger' must have IsFrozen set to false to modify
A workaround is to move the interaction block to a parent element, e.g. BeginStoryboard and refer to the Storyboard with an ElementName binding.
<BeginStoryboard>
<b:Interaction.Triggers>
<b:EventTrigger EventName="Completed" SourceObject="{Binding ElementName=StartNewGameAnimation}">
<b:InvokeCommandAction Command="{Binding StartGameWhenStartanimationCompletedCommand}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
<!-- ...other code. -->
</Storyboard>
</BeginStoryboard>
Depending on the context, setting the SourceName instead of SourceObject works, too.
<b:EventTrigger EventName="Completed" SourceName="StartNewGameAnimation">
By the way, you have to add the following XML namespace in your control, to refer to the types:
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
Whenever a node is selected in my treeview, it automatically does a horizontal scroll to that item. I've found the way to disable this. If I use this code in code behind, it works perfectly:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
However, if I use MVVM, I cannot disable horizontal scroll to the item:
My window:
<Window x:Class="TreeViewWpfApplication.MainWindow"
. . . . .
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:ei=http://schemas.microsoft.com/expression/2010/interactions>
<TreeView Grid.Column="1" Margin="5" Background="Green">
<i:Interaction.Triggers>
<i:EventTrigger EventName="RequestBringIntoView">
<ei:CallMethodAction MethodName="RequestBringIntoView_Handler" TargetObject="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TreeView>
<TreeViewItem Header="---Level 1" >
<TreeViewItem Header="--- Level 2.1" >
<TreeViewItem Header="--- Level 3.1" >
</TreeViewItem>
</TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="Level 2.3" />
</TreeView>
</Window>
ViewModel:
public void RequestBringIntoView_Handler(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
Why cannot I stop automatic horizontal scroll to the item by MVVM approach?
I believe you can always loop through all TreeViewItem of the TreeView and clone each TriggerBase from Interaction.Triggers attached on the TreeView before attaching each cloned one to the Interaction.Triggers of each TreeViewItem.
I'm so disappointed about Microsoft, that name has made me proud of and been my endless inspiration for many years since I started beginning to learn how to program. But frankly speaking there are many things Microsoft made us disappointed. Your code should have actually worked fine. Why? I've tried it and the event RequestBringIntoView actually bubbles up from TreeViewItem to TreeView. And in fact when you add the event handler on TreeView directly, you'll see the event handler is fired OK. But the very equivalent form of setting handler using Interaction does not work that way. That's so terrible. It's obvious that it's designed to setup event handler in MVVM way but it's so limited.
I had to make a work-around in which we use a custom attached property to allow to set the Interaction.Triggers in Style. However I have to say that it's not very pretty. You need to explicitly declare an Array of TriggerBase (I've done something like this before but never found a better solution for this). Next you need to use a proxy to bind TargetObject for EventTrigger (because we put the trigger in an Array and it's detached from visual tree).
Here is the code for the custom attached property:
//add some using alias like this first
//using i = System.Windows.Interactivity;
public static class InteractionX
{
public static readonly DependencyProperty TriggersProperty
= DependencyProperty.RegisterAttached("Triggers", typeof(i.TriggerBase[]),
typeof(InteractionX), new PropertyMetadata(triggersChanged));
public static i.TriggerBase[] GetTriggers(DependencyObject o){
return o.GetValue(TriggersProperty) as i.TriggerBase[];
}
public static void SetTriggers(DependencyObject o, i.TriggerBase[] value)
{
o.SetValue(TriggersProperty, value);
}
static void triggersChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var triggers = e.NewValue as i.TriggerBase[];
var currentTriggers = i.Interaction.GetTriggers(o);
currentTriggers.Clear();
foreach (var t in triggers)
{
t.Detach();
currentTriggers.Add(t);
}
}
}
Here is the XAML:
<TreeView>
<TreeView.Resources>
<DiscreteObjectKeyFrame x:Key="proxy" Value="{Binding}"/>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="local:InteractionX.Triggers">
<Setter.Value>
<x:Array Type="{x:Type i:TriggerBase}">
<i:EventTrigger EventName="RequestBringIntoView">
<ei:CallMethodAction MethodName="bringIntoViewHandler"
TargetObject="{Binding Value, Source={StaticResource proxy}}"/>
</i:EventTrigger>
</x:Array>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
It appears that the Interaction.Triggers set on TreeViewItem can handle bubbling-up RequestBringIntoView from the descendant TreeViewItems but as I said it's a pity that setting that on TreeView does not work.
I realize my own Calendar.
I did a Generic.xaml (ResourceDictionnary) which contains my new control. I have a Calendar.class who implement :Control.
In my Calendar class I have a ObservableCollection<Day> _days. I put DataContext = this;
A Day contains my ObservableCollection<MyObject> ListeTache.
And Day.Class Implement INotifyPropertyChanged and have my event :
public event PropertyChangedEventHandler PropertyChanged;
But when I update my Listbox, I have to reload my calendar manually to see any changes.
Am I missing something ?
Thank you for the help.
My ObservableCollection<MyObject> :
public ObservableCollection<Tache> ListeTache
{
get { return this._listeTache; }
set
{
_listeTache = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("ListeTache"));
}
}
}
My Generic.xaml look like this :
<Grid x:Name="LayoutTache">
<ListBox x:Name="ListeTaches" ItemsSource="{Binding ListeTache,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" FontSize="10" PreviewMouseDown="PreviewMouseDownClick_clear" MouseDoubleClick="doubleClic" IsSynchronizedWithCurrentItem="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding AffichageCalendrier}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.Resources>
<ContextMenu x:Key="MonMenu">
<MenuItem Header="Supprimer" Click="MonMenuDel_Click" />
</ContextMenu>
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="AntiqueWhite"></Setter>
<Setter Property="ContextMenu" Value="{StaticResource MonMenu}"/>
</Trigger>
</Style.Triggers>
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="LightGreen" />
</Style.Resources>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
After somme reply :
How I can do that ? I have to add something like this in my Day.cs class :
_listeTache.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_listeTache_CollectionChanged);
void _listeTache_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
throw new NotImplementedException();
}
I never catch the event...
Thanks for all
When designing a custom control, it is customary not to set the DataContext to this... in fact, don't set it to anything as this enables it to be set from outside the control. Instead, you should reference your property from generic.xaml using a RelativeSource Binding:
<ListBox x:Name="ListeTaches" ItemsSource="{Binding ListeTache, RelativeSource={
RelativeSource AncestorType={x:Type YourXamlNamespacePrefix:Calendar}}}" ... />
It should also be noted that using UpdateSourceTrigger=PropertyChanged, Mode=TwoWay on an ItemsSource Binding is pointless as the ItemsControl cannot update the source collection.
If you still can't access the property, then you must either ensure that you correctly implement the INotifyPropertyChanged interface in Calendar.cs, or you can implement your ListeTaches property as a DependencyProperty instead.
UPDATE >>>
You've clearly done something wrong... it's really not that complicated. Follow the links that I provided to declare a DependencyProperty in your Calendar.cs class. Do not set the DataContext. Use the RelativeSource Binding that I showed you, correctly setting up the proper XAML Namespace... that's it!
Just one last thing... you did add a WPF Custom Control Library project into your application, didn't you? You need to have something like this in your Calendar class' static constructor:
static Calendar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Calendar),
new FrameworkPropertyMetadata(typeof(Calendar)));
}
Perhaps it would help if you read through the Control Authoring Overview page on MSDN to ensure that you are doing it correctly?
UPDATE 2 >>>
Ok, so after noticing something in your comments, I think that I know what your problem is. #XAMlMAX asked you
have you tried removing that null check for your PropertyChanged in Listtache?
You replied
When I remove it, I catch TargetInvocationException.
I think that that's your problem... that means that your PropertyChanged event is null... that means that you have not attached a handler to it yet... it's not being used. Try attaching a handler to it and your ListeTache collection should display fine.
I'm using this as a basis to make an animation start using code behind. Based on the contents of the article, I have the following:
<Window.Resources>
<Storyboard x:Key="sbdLabelRotation">
<DoubleAnimation
Storyboard.TargetName="lblHello"
Storyboard.TargetProperty="(TextBlock.RenderTransform).(RotateTransform.Angle)"
From="0"
To="360"
Duration="0:0:0.5"
RepeatBehavior="4x" />
</Storyboard>
</Window.Resources>
I have the following XAML (obviously):
<Label x:Name="lblHello" Content="test" Margin="20"/>
And the code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public void AnimateLabelRotation()
{
Storyboard sbdLabelRotation = (Storyboard)FindResource("sbdLabelRotation");
sbdLabelRotation.Begin(this);
}
Which I call from a button click event. The FindResource works and finds the storyboard, but nothing happens. I have managed to get the animation to work on an event trigger, but clearly I'm missing something for the code behind.
This:
<Label x:Name="lblHello" Content="test" Margin="20"/>
and this:
Storyboard.TargetProperty="(TextBlock.RenderTransform).(RotateTransform.Angle)"
are not compatible.
When the animation tries to find the property to animate, it goes to (TextBlock.RenderTransform) and finds null since you didn't declare it (actually it doesn't since you say TextBlock but apply it to Label, more on that later in the answer). Thus it cannot find .(RotateTransform.Angle).
To remedy the issue:
<Label x:Name="lblHello"
Content="test"
Margin="20"
RenderTransformOrigin="0.5,0.5">
<Label.RenderTransform>
<RotateTransform />
</Label.RenderTransform>
</Label>
Notice RenderTransformOrigin setting - this means that the axis of rotation will be in the center of the object (X and Y).
Also, in the animation it should be:
Storyboard.TargetProperty="(Label.RenderTransform).(RotateTransform.Angle)"
There is a link to download the whole project
http://www.galasoft.ch/mydotnet/articles/resources/article-2006102701/GalaSoftLb.Article2006102701.zip
You can study the code and see it running. Sometimes it's more helpful.
Also in your code the part:
sbdLabelRotation.Begin(this);
could be wrong. As you know the this keyword references the class itself, in your case the MainWindow class. You should try without the this keyword.
I have a control defined in Silverlight as follows:
<HyperlinkButton x:Name="testHyperlink" Content="Test" FontWeight="Bold" Click="testHyperlink_Click">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction PropertyName="Visibility" TargetName="panel1"
Value="Collapsed" />
<ei:ChangePropertyAction PropertyName="Visibility" TargetName="panel2"
Value="Visible" />
</i:EventTrigger>
<i:EventTrigger>
<ei:ChangePropertyAction PropertyName="Visibility" TargetName="panel1"
Value="Visible" />
<ei:ChangePropertyAction PropertyName="Visibility" TargetName="panel2"
Value="Collapsed" />
</i:EventTrigger>
</i:Interaction.Triggers>
</HyperlinkButton>
This hyperlink is part of a DataTemplate. That is the reason I'm using the triggers. When someone clicks the HyperlinkButton, an asynchronous process is fired. When the process has completed, I want to execute the second trigger. Essentially, I'm flipping the visibility of some content.
My question is, when my event is finished, how do I fire the second EventTrigger associated with the HyperlinkButton?
It's incorrect using of Interactivity EventTriggers. Answering directly your question, you can do next (I'm writting that only because I coudn't write that it's impossible, but I'm ashamed for this solution):
create own action with public Invoke
public class MyChangePropertyAction: ChangePropertyAction
{
public new void Invoke(object parameter)
{
base.Invoke(parameter);
}
}
Use it instead Interactivity ChangePropertyAction. Now you can get invoke action directly from code behind:
((MyChangePropertyAction)Interaction.GetTriggers(testHyperlink)[1]).Invoke(parameter);
But, I believe that you can simply use MVVM approach and do next:
create bool property IsBusy with property changed notification in view model;
bind it to your "panel1" Visibility property via BooleanToVisibility converter;
bind command DoServiceCall from view model to "testHyperlink" Command property;
and in view model make service calls and change IsBusy property to true or false depending on should you display panel or not.
Good luck