In a WPF Application, I have a ViewModel that is exposing a collection of strings that I'm displaying as buttons through an ItemsControl container using a WrapPanel. I haven't been able to bind the RelayCommand in my ViewModel to the buttons, however.
ViewModel (IncidentAddressesViewModel):
public IEnumerable<string> Addresses { get; set; }
public RelayCommand<string> ZoomToAddressCommand { get {
if (this.zoomToAddressCommand == null) this.zoomToAddressComamnd = new RelayCommand<string>(this.ZoomToAddress);
return this.zoomToAddressCommand;
}}
private void ZoomToAddress(string address) { MessageBox.Show (address); }
XAML:
<TabItem x:Name="IncidentAddressesTab">
<ItemsControl ItemsSource="{Binding Addresses}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Command">
<cmd:EventToCommand
Command="{Binding ZoomToAddressCommand}"
CommandParameter="{Binding Text}"
PassEventArgsToCommand="True"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</TabItem>
Xaml Code Behind that connects the DataContext
IncidentAddressesTab.DataContext = new IncidentAddressesViewModel();
The buttons are showing with the addresses. When I set a breakpoint at the ZoomToAddressCommand, it does get hit once, but when I click the buttons, the ZoomToAddress method never gets invoked.
UPDATE to include binding details:
I am actually binding to the TabItem. I have updated the XAML to include the additional tag and added the binding code in the XAML Code Behind. I didn't know this was pertinent information or I would have added it in the beginning.. (:
It doesn't work because you have tried to Bind the Command to the ItemsControl and not the Button controls. Have you tried this?:
<DataTemplate>
<Button Content="{Binding}" Command="{Binding DataContext.ZoomToAddressCommand,
RelativeSource={RelativeSource AncestorType={x:Type
YourViewNamespace:YourViewName}}}" />
</DataTemplate>
What we're attempting to do here is to Bind from the DataTemplate to the view model that I am assuming is set as the DataContext of the current view. Please replace "YourViewNamespace:YourViewName" with the actual names of your XML namespace and view.
UPDATE >>>
Ok, after looking at your code again, I can see that you are Binding to the Addresses collection simply using the property name. You say that the DataContext is set on the ItemsControl, so I'm assuming that by that you mean that your view model is set on the ItemsControl.DataContext property. If that is so, then we need to change our Binding to the Command like this:
<DataTemplate>
<Button Content="{Binding}" Command="{Binding DataContext.ZoomToAddressCommand,
RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
</DataTemplate>
If your view model is not set on the ItemsControl.DataContext property, then this will not work and you will need to clearly tell me how you have connected your view model to your view. Before I just assumed that your view model was data bound to the DataContext of the containing view or Window as is normally done... maybe next time, you can provide this information in your question to make it easier for people to answer it?
UPDATE 2 >>>
Ok, you've updated the question with the essential DataContext information... perfect. Now I can answer your question properly without all the guessing... do you see how much easier this would have been if you had added that there in the first place? No matter... we're here now. Try this final example:
<DataTemplate>
<Button Content="{Binding}" Command="{Binding DataContext.ZoomToAddressCommand,
RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}" />
</DataTemplate>
To reiterate... this RelativeSource Binding will look up the visual tree until it finds a TabItem control. Then, it will look at the DataContext property of that control. Finally, it will look for a ZoomToAddressCommand property in the object (your view model) that is set as the DataContext of the TabItem... and there we are.
I ended up having to change the structure a little bit,
I added a class:
public class IncidentAddress {
public string Address { get; set; }
private RelayCommand zoomCommand;
public RelayCommand ZoomCommand {
get {
if (zoomCommand == null)
zoomCommand = new RelayCommand(Zoom);
return zoomCommand;
}
}
public void Zoom() {
MessageBox.Show(Address);
}
}
In my ViewModel,
this:
public IEnumerable<string> Addresses { get; set; }
changed to:
public IEnumerable<IncidentAddress> Addresses { get; set; }
and I removed the RelayCommand from the ViewModel and left it in the new class
The XAML ended up being this:
<TabItem x:Name="IncidentAddressesTab">
<ItemsControl ItemsSource="{Binding Addresses}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Address}" Command="{Binding ZoomCommand}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</TabItem>
Related
I'm making TabControl that can change dynamically using ItemsSource tag.
I want to know the way to access ViewModel data in ItemsSource tag.
I searched through the Internet. but I couldn't find the answer.
CODE
public class ViewModel
{
// this will be used in ItemsSource
private ObservableCollection<ActiveButton> _allExecuteButtonInfos = new ObservableCollection<ActiveButton>();
public ObservableCollection<ActiveButton> AllExecuteButtonInfos
{
get { return _allExecuteButtonInfos; }
set {
_allExecuteButtonInfos = value;
OnPropertyChanged();
}
}
// I want to get this data in ItemsSource
private List<string> _boardNameList = new List<string>();
public string BoardNameList
{
get { return _boardNameList; }
set {
_boardNameList = value;
OnPropertyChanged();
}
}
}
XAML
<Grid>
<TabControl Background="#FF292929" ItemsSource="{Binding AllExecuteButtonInfos}">
<TabControl.ContentTemplate>
<DataTemplate>
<ScrollViewer Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
<Grid VerticalAlignment="Stretch" Margin="0,0,0,0" >
<ComboBox Width="334" Margin="0,0,0,0" Grid.Column="1" Grid.Row="1" Height="22" VerticalAlignment="Top"
<!-- I want to get data from ViewModel not in ItemsSource(AllExecuteButtonInfos) -->
<!-- eg) VM:BoardNameList, ViewModel.BoardNameList etc -->
ItemsSource="{Binding BoardNameList, Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedBoard, Mode=TwoWay}"/>
</Grid>
</ScrollViewer>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
I hope I can find the answer.
Thank you.
You could bind to the DataContext, i.e. the view model, of the parent TabControl using a RelativeSource:
<ComboBox ...
ItemsSource="{Binding DataContext.BoardNameList, RelativeSource={RelativeSource AncestorType=TabControl}}" />
Note that it's pointless to set the Mode of an ItemsSource binding to TwoWay since the control never sets the property. It's also meaningless to set the UpdateSourceTrigger to PropertyChanged in this case for the same reason.
I am not sure where you've defined the data context but I suppose that it's somewhere above the first 'Grid' markup. Something like this?
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
Then you have to somehow refer to the Datacontext of the window. You can do it this way
<ComboBox
ItemsSource="{Binding DataContext.BoardNameList, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
if the name of your view is not 'MainWindow', you have to change it to the view name where you have that code.
One of the best ways is to create a UserControl for each model and then put data templates in TabControl.Resources with DataType specified for all types you could put in ItemsSource - you get full customization of the view with nice seperation of XAML files.
<Grid>
<TabControl Background="#FF292929" ItemsSource="{Binding AllExecuteButtonInfos}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type MyViewModel1}">
<MyViewModel1_View ViewModel="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type MyViewModel2}">
<MyViewModel2_View ViewModel="{Binding}"/>
</DataTemplate>
</TabControl.Resources>
</TabControl>
</Grid>
I'm going from memory, so the binding may be done differently, but that's the basic idea.
That, or you use some kind of ViewResolver as the only item in the TabControl (something like this)
Basically, go even more MVVM :)
Provided that the DataContext of your view is set correctly to your ViewModel and AllExecuteButtonInfos is indeed available in your view, you can use a RelativeBinding to access properties which are not in the DataContext of your current scope.
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.BoardNameList}" />
With that, you are leaving the implicit DataContext of the DataTemplate, which is ActiveButton and access the object of the specified type via AncestorType. From there you can set a Path to the DataContext of the UserControl, which is, in your case, an object of the class ViewModel.
Imaging you are climbing up a ladder. From the ComboBox object up to your UserControl, from where you can access all underlying properties.
I'm structuring my WPF application using MVVM Light and am creating the ViewModel using the IOC.
The page initializes its DataContext like this:
DataContext="{Binding Main, Source={StaticResource Locator}}"
A TabControl has its content bound to another ViewModel, so binding from within the TabControl will access the tab ViewModel by default.
Now, how can I instead access the page ViewModel in XAML?
Before switching to use IOC, the ViewModel was created as a StaticResource and I could access it like this
Zoom="{Binding Zoom, Source={StaticResource ViewModel}, Mode=TwoWay}"
Then I could also access it via the Locator, however I don't like this syntax as what happens if this ViewModel instance was created with a key? I don't think the content binding should care about such details.
Zoom="{Binding Main.Zoom, Source={StaticResource Locator}, Mode=TwoWay}"
What's the right way of doing it?
You can use RelativeSource Binding with Mode set to FindAncestor. This will allow you to bind to the DataContext of your window (or any other element that contains your tab control) without knowing anything about it.
I set up a simple example based on your description.
I have 2 simple View Models:
public class MainViewModel : ViewModelBase
{
public double Zoom { get; } = 1;
}
public class TabViewModel : ViewModelBase
{
public double Zoom { get; } = 2;
}
And here is the content of my xaml:
<Window
...blah blah blah...
DataContext="{Binding Main, Source={StaticResource Locator}}"
>
<Grid>
<TabControl>
<TabItem DataContext="{Binding Tab, Source={StaticResource Locator}}" Header="TabItem">
<StackPanel>
<Label Content="{Binding DataContext.Zoom, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}" />
<Label Content="{Binding Zoom}" />
</StackPanel>
</TabItem>
</TabControl>
</Grid>
The first label gets it's value from the MainViewModel, and the second - from the TabViewModel.
The one downside I found is design time data for such a binding does not work properly. This can be solved by providing a fallback value.
Hope this solves your problem.
Short explanation of need: I need to fire the command of a button inside a DataTemplate, using a method from the DataContext of the ViewModel.
Short explanation of problem: The templated button command only seems to be bindable to the datacontext of the item itself. The syntax used by WPF and Windows 8.1 apps to walk up the visual tree doesn't seem to work, including ElementName and Ancestor binding. I would very much prefer not to have my button command located inside the MODEL.
Side Note: This is built with the MVVM design method.
The below code generates the list of items on the VIEW. That list is one button for each list item.
<ItemsControl x:Name="listView" Tag="listOfStories" Grid.Row="0" Grid.Column="1"
ItemsSource="{x:Bind ViewModel.ListOfStories}"
ItemTemplate="{StaticResource storyTemplate}"
Background="Transparent"
IsRightTapEnabled="False"
IsHoldingEnabled="False"
IsDoubleTapEnabled="False"
/>
Inside the page resources of the same VIEW, I have created a DataTemplate, containing the problematic button in question. I went ahead and stripped out most of the formatting inside the button, such as text, to make the code easier to read on this side. Everything concerning the button works, except for the problem listed, which is the binding of the command.
<Page.Resources>
<DataTemplate x:Name="storyTemplate" x:DataType="m:Story">
<Button
Margin="0,6,0,0"
Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
HorizontalContentAlignment="Stretch"
CommandParameter="{Binding DataContext, ElementName=Page}"
Command="{Binding Source={StaticResource Locator}}">
<StackPanel HorizontalAlignment="Stretch" >
<TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
FontSize="30"
TextTrimming="WordEllipsis"
TextAlignment="Left"/>
</StackPanel>
</Button>
</DataTemplate>
</Page.Resources>
Because this is a DataTemplate, the DataContext has been set to the individual items that comprise the list (MODEL). What I need to do is select the DataContext of the list itself (VIEWMODEL), so I can then access a navigation command.
If you are interested in the code-behind of the VIEW page, please see below.
public sealed partial class ChooseStoryToPlay_View : Page
{
public ChooseStoryToPlay_View()
{
this.InitializeComponent();
this.DataContextChanged += (s, e) => { ViewModel = DataContext as ChooseStoryToPlay_ViewModel; };
}
public ChooseStoryToPlay_ViewModel ViewModel { get; set; }
}
I've tried setting it by ElementName, among many other attempts, but all have failed. Intellisense detects "storyTemplate" as an option when ElementName is input, which is the name of the DataTemplate shown in the first code block of this question.
I don't believe my problem can be unique, however I'm having great difficulty finding a solution for UWP. Allow me to apologize in advance in this is a simple question, but I've spent nearly two days researching answers, with none seeming to work for UWP.
Thank you guys!
What MVVM toolkit are you using (if any)? In MVVM Light, you can get a hold of ViewModel from DataTemplate same way you set DataContext for your view:
<DataTemplate x:Key="SomeTemplate">
<Button Command="{Binding Main.MyCommand, Source={StaticResource ViewModelLocator}}"/>
</DataTemplate>
It really is unfortunate that there is no ancestor binding in UWP. This makes scenarios like yours much more difficult to implement.
The only way I can think of is to create a DependencyProperty for ViewModel on your Page:
public ChooseStoryToPlay_ViewModel ViewModel
{
get { return (ChooseStoryToPlay_ViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(ChooseStoryToPlay_ViewModel), typeof(MainPage), new PropertyMetadata(0));
Now you can bind to it from your data template:
<DataTemplate x:Name="storyTemplate" x:DataType="local:Story">
<Button
Margin="0,6,0,0"
Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
HorizontalContentAlignment="Stretch"
CommandParameter="{x:Bind Page}"
Command="{Binding ViewModel.NavigateCommand, ElementName=Page}">
<StackPanel HorizontalAlignment="Stretch" >
<TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
FontSize="30"
TextTrimming="WordEllipsis"
TextAlignment="Left"/>
</StackPanel>
</Button>
</DataTemplate>
A couple of things to notice:
In CommandParameter I assumed that in your Story class there is a Page property that you want to pass as a parameter to your command. You can bind to any other property of Story class here or the class itself.
You have to set the name of your page to Page (x:name="Page"), so that you can reference it using ElementName in the data template.
I assumed that the command you're calling on the ViewModel is named NavigateCommand and accepts a parameter of the same type as the property bound to CommandParameter:
public ICommand NavigateCommand { get; } =
new RelayCommand<string>(name => Debug.WriteLine(name));
I hope this helps and is applicable to your scenario.
There is a few ways to do that. But i think the Command change better...
Example, you have a (grid,list)view with some itemtemplate like that:
<GridView.ItemTemplate>
<DataTemplate>
<Grid
x:Name="gdVehicleImage"
Height="140"
Width="140"
Background="Gray"
Margin="2"
>
</Grid>
</GridView.ItemTemplate>
And do you want to make a command to for example a FlyoutMenu... But the command it's in the ViewModel and not in GridView.SelectedItem...
What you can do is...
<Grid
x:Name="gdVehicleImage"
Height="140"
Width="140"
Background="Gray"
Margin="2"
>
<FlyoutBase.AttachedFlyout>
<MenuFlyout
Opened="MenuFlyout_Opened"
Closed="MenuFlyout_Closed"
>
<MenuFlyout.MenuFlyoutPresenterStyle>
<Style TargetType="MenuFlyoutPresenter">
<Setter Property="Background" Value="DarkCyan"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</MenuFlyout.MenuFlyoutPresenterStyle>
<MenuFlyoutItem
Loaded="mfiSetAsDefaultPic_Loaded"
CommandParameter="{Binding}"
/>
<MenuFlyoutItem
Loaded="mfiDeletePic_Loaded"
CommandParameter="{Binding}"
/>
</MenuFlyout>
</FlyoutBase.AttachedFlyout>
</Grid>
And in the loaded events:
private void mfiDeletePic_Loaded(object sender, RoutedEventArgs e)
{
var m = (MenuFlyoutItem)sender;
if (m != null)
{
m.Command = Vm.DeleteImageCommand;
//Vm is the ViewModel instance...
}
}
Is not entirely beautiful... But you willnot breake mvvm pattern like this...
I'm interested in creating an app that displays some buttons and changes a viewport according to the selected button. The viewport in my app is a ContentControl and I thought of changing its content whenever a button is clicked. However, I believe there's a better approach, by perhaps injecting the ViewModels of each of the Views I want to present to the ContentControl and styling them using DataTemplates (Since I want to avoid having a grid with many controls and just setting their Visibility property whenever I want to show a particular view). Which of the approaches seems better to you? Do you have a different approach for this?
The view should be something similar to this:
Thanks!
Usually have a ViewModel behind the window which contains:
ObservableCollection<IViewModel> AvailableViewModels
IViewModel SelectedViewModel
ICommand SetCurrentViewModelCommand
I display the AvailableViewModels using an ItemsControl, which has its ItemTemplate set to a Button. The Button.Command is bound to the SetCurrentViewModelCommand, and it passes the current data item from the AvailableViewModels collection in through the CommandParameter
To display the content area, I use a ContentControl with ContentControl.Content bound to SelectedViewModel, and DataTemplates get used to tell WPF how to render each ViewModel.
The end result is my XAML looks something like this:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ViewModelA}">
<local:ViewA />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModelB}">
<local:ViewB />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding AvailableViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.SetCurrentViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ContentControl Content="{Binding SelectedViewModel}" />
</DockPanel>
You can view an example of the full code used for such a setup on my blog
I am busy creating my first MVVM application in WPF.
Basically the problem I am having is that I have a TreeView (System.Windows.Controls.TreeView) which I have placed on my WPF Window, I have decide that I will bind to a ReadOnlyCollection of CommandViewModel items, and these items consist of a DisplayString, Tag and a RelayCommand.
Now in the XAML, I have my TreeView and I have successfully bound my ReadOnlyCollection to this. I can view this and everything looks fine in the UI.
The issue now is that I need to bind the RelayCommand to the Command of the TreeViewItem, however from what I can see the TreeViewItem doesn't have a Command. Does this force me to do it in the IsSelected property or even in the Code behind TreeView_SelectedItemChanged method or is there a way to do this magically in WPF?
This is the code I have:
<TreeView BorderBrush="{x:Null}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TreeView.Items>
<TreeViewItem
Header="New Commands"
ItemsSource="{Binding Commands}"
DisplayMemberPath="DisplayName"
IsExpanded="True">
</TreeViewItem>
</TreeView.Items>
and ideally I would love to just go:
<TreeView BorderBrush="{x:Null}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TreeView.Items>
<TreeViewItem
Header="New Trade"
ItemsSource="{Binding Commands}"
DisplayMemberPath="DisplayName"
IsExpanded="True"
Command="{Binding Path=Command}">
</TreeViewItem>
</TreeView.Items>
Does someone have a solution that allows me to use the RelayCommand infrastructure I have.
Thanks guys, much appreciated!
Richard
I know this was "answered" a while ago, but since the answers weren't ideal, I figured I'd put in my two cents. I use a method that allows me to not have to resort to any "styled button trickery" or even using code-behind and instead keeps all my separation in MVVM. In your TreeView add the following xaml:
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding TreeviewSelectedItemChanged}" CommandParameter="{Binding ElementName=treeView, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In your xaml header add:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
and then you'll have to add a reference to the above assembly in your project.
After that, everything acts just the same as any other command would on say a button or something.
Thanks for the input into the issue, and yes, I did say I didn't want a Code behind solution, however at that time I was still very much under the impression that I was simply missing something... so I ended up using the TreeView_SelectedItemChanged event.
Even though Will's approach seems like a good work around, for my personal situation I decided that I would use the code behind. The reason for this is so that the View and XAML would remain as it would be if the TreeViewItem had a "Command" property to which my Command could be bound. Now I do not have to change the Templates or the Views, all I have to do is add the code and the Event for the TreeView_SelectedItemChanged.
My solution:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (sender != null)
{
var treeView = sender as TreeView;
if (treeView != null)
{
var commandViewModel = treeView.SelectedItem as CommandViewModel;
if (commandViewModel != null)
{
var mi = commandViewModel.Command.GetType().GetMethod("Execute");
mi.Invoke(commandViewModel.Command, new Object[] {null});
}
}
}
}
As I already have the RelayCommand attached to the TreeViewItem, all I am now doing is to just manually invoke the "Execute" method on that specific RelayCommand.
If this is the completely wrong way of going about it then please let me know...
Thanks!
What I'd do is set the Header of the TreeViewItem to be a button, then skin the button so that it doesn't look or act like one, then perform my command binding against the button.
You might need to do this via a DataTemplate, or you might need to change the template of the TreeViewItem itself. Never done it, but this is how I've done similar things (such as tab page headers).
Here's an example of what I'm talking about (you can drop this in Kaxaml and play around with it):
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<Style x:Key="ClearButan" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="border"
Padding="4"
Background="transparent">
<Grid >
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center">
</ContentPresenter>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Page.Resources>
<Grid>
<TreeView>
<TreeViewItem>
<Button Style="{StaticResource ClearButan}">
easy peasy
</Button>
</TreeViewItem>
</TreeView>
</Grid>
</Page>
I've created a new clear style for a button. I then just drop a button in the TVI and set its style. You can do the same thing using data templates, of course.
This is a good example of how the MVVM is very much an after-thought in WPF. You expect there to be Command support of certain gui items, but there isn't, so you're forced to go through an elaborate process (as shown in Will's example) just to get a command attached to something.
Let's hope they address this in WPF 2.0 :-)
I improve good solution from Richard via common Tag property:
MyView.xaml:
<TreeView SelectedItemChanged="TreeView_SelectedItemChanged" Tag="{Binding SelectTreeViewCommand}" >
<TreeViewItem Header="Item1" IsExpanded="True" Tag="Item1" />
<TreeViewItem Header="Item2" IsExpanded="True">
<TreeViewItem Header="Item21" Tag="Item21"/>
</TreeViewItem>
</TreeView>
MyView.xaml.cs
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = (TreeView)sender;
var command = (ICommand)treeView.Tag;
TreeViewItem selectedItem = (TreeViewItem)treeView.SelectedItem;
if (selectedItem.Tag != null)
{
command.Execute(selectedItem.Tag);
}
}
MyViewModel.cs
public RelayCommand selectTreeViewCommand;
[Bindable(true)]
public RelayCommand SelectTreeViewCommand => selectTreeViewCommand ?? (selectTreeViewCommand = new RelayCommand(CanSelectTreeViewCommand, ExecuteSelectTreeViewCommand));
private void ExecuteSelectTreeViewCommand(object obj)
{
Console.WriteLine(obj);
}
private bool CanSelectTreeViewCommand(object obj)
{
return true;
}
The answer provided by Shaggy13spe is very good. But still, it took me some additional time to understand it so I will extend the answer.
Whole TreeView xaml can look like this:
<TreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Tree}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding FilterMeetingsCommand}" CommandParameter="{Binding ElementName=treeView, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Nodes}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"></TextBlock>
<TextBlock Text="{Binding Id}"></TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
In my View I have a Tree collection
public ObservableCollection<TreeNode> Tree { get; set; }
TreeNode is defined as a simple class:
public class TreeNode
{
public int Id { get; set; }
public string Name { get; set; }
public List<TreeNode> Nodes { get; set; }
public TreeNode(string name)
{
this.Name = name;
this.Nodes = new List<TreeNode>();
}
}
First important point: CommandParameter is not bind to the property on the ViewModel but it is passed to the method. So the method should look like:
private async void FilterMeeting(object parameter){}
Second important point: if you will pass the selected item (in my case object will be TreeNode type) and you will have the hierarchical structure you will face event bubbling. So selecting an item will fire the event for this particular item and for all parents. To resolve this you need to understand that you can pass only one object to the method in ViewModel (not two as in standard event handler) and this object needs to be an event.
In this case change the XAML to following (PassEventArgsToCommand="True" is important here)
<TreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Tree}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding FilterMeetingsCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Nodes}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"></TextBlock>
<TextBlock Text="{Binding Id}"></TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Then in your handling method, you won't receive the model object, but event args, which have a model object inside.