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...
Related
I have the following XAML...
<TabControl Grid.Row="0" Grid.Column="1" Grid.RowSpan="2"
Name="customerTab"
ItemsSource="{ Binding DetailViewModels }"
SelectedItem="{Binding SelectedDetailViewModel, Mode=TwoWay}"
TabStripPlacement="Top">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}" />
<TextBlock Text="*" Visibility="{Binding HasChanges, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Command="{Binding CloseCommand}" Style="{StaticResource closeButtonStyle}" />
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
Basically, I have a listview where I can click on a record to view detail. The detail record gets displayed in the tab control.
You can see I have a button which is bound to a command that closes the tab.
When I close the tab, the following binding error displays...
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TabControl', AncestorLevel='1''. BindingExpression:Path=TabStripPlacement; DataItem=null; target element is 'TabItem' (Name=''); target property is 'NoTarget' (type 'Object')
I am not totally sure of the issue. Does this mean that the detail viewmodels cannot climb back up the hierarchy to the tab control when it closes?
The actual application works as designed, I just want to address this error so it does not keep coming up every time I close a tab.
The CloseCommand is a delegate command. Here is that code along with the method that it runs.
public DelegateCommand CloseCommand { get; private set; }
CloseCommand = new DelegateCommand(OnClose);
public void OnClose()
{
OnTabClosed?.Invoke(InstanceId);
}
OnTabClosed is an action that closes the tab and the InstanceId is simply a GUID of the detail viewmodel.
I did search online and found a way to hide the message, but I am disinclined to do that for fear of hiding more legitimate binding errors.
How do I fix this? What is the best way to debug?
Edit
Here is the code that handles the closing of the tab item...
private void HandleTabClosed(Guid instanceId)
{
DetailViewModels.Remove(DetailViewModels.First(vm => vm.InstanceId == instanceId));
}
I just made the test, and I find no error, but I do it a bit differently, so hope will be good for you :
In XAML, I use a "Tag" for buttons (very useful when you use templates).
Taking your code, that would be something like that :
<TabControl Grid.Row="0" Grid.Column="1" Grid.RowSpan="2"
Name="customerTab"
ItemsSource="{ Binding DetailViewModels }"
SelectedItem="{Binding SelectedDetailViewModel, Mode=TwoWay}"
TabStripPlacement="Top">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}" />
<TextBlock Text="*" Visibility="{Binding HasChanges, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Content="X" Click="Button_Click" Tag="{Binding InstanceId}" />
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
Then on my "OnClick" event I will look which Instance I may remove, then remove it :
private void Button_Click(object sender, RoutedEventArgs e)
{
Button item = (Button)sender;
long instanceId = (long)item.Tag;
GroupResults selectedGroup = this.MyFullList.FirstOrDefault(x => x.InstanceId == instanceId);
if(selectedGroup!=null)
{
this.MyFullList.Remove(selectedGroup);
}
}
It's not really the same way as you did, but it works fine. I always use Tags when I work on templates, and until now never got any problem.
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 struggling to solve this problem, i'm trying to handle the PreviewMouseLeftButtonDown & MouseEnter & MouseLeave event inside my bound ListBox. Currently learning WPF.
The Image is inside my ListBox with other Controls here's a Picture for clarification.
My Problem is the two Image Controls are not known in Code behind because they are inside a DataTemplate and thats why i cant handle them.
Heres my Xaml Code:
<ListBox Name="ListBoxDownload" Height="414" Width="729" Canvas.Left="-3" Visibility="Collapsed">
<ListBox.ItemTemplate>
<DataTemplate>
<Canvas Height="89" >
<Canvas Height="86" Width="11" Background="#FFC33232" Canvas.Left="-2"/>
<ProgressBar Width="694" Canvas.Left="20" Canvas.Top="76" Height="10" Value="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Maximum="{Binding Maximum}" Minimum="0"/>
<Label Foreground="White" FontFamily="/SpotWatch;component/Resources/Fonts/#Montserrat Light" FontSize="18" Content="{Binding Name}" Canvas.Left="14" Canvas.Top="-4"/>
<Label Foreground="#FFC3BDBD" FontFamily="/SpotWatch;component/Resources/Fonts/#Montserrat Ultra Light" FontSize="14" Content="{Binding Artist}" Canvas.Left="14" Canvas.Top="25"/>
<Label Foreground="#FF8D8D8D" FontFamily="/SpotWatch;component/Resources/Fonts/#Montserrat Ultra Light" FontSize="12" Content="{Binding Status}" Canvas.Left="14" Canvas.Top="50"/>
<Image Name="ImageDeleteSong" Source="/Resources/Images/SpotWatch.Delete.png" Canvas.Left="675" Canvas.Top="6" Width="17" Height="19" MouseEnter="ImageDeleteSong_MouseEnter" MouseLeave="ImageDeleteSong_MouseLeave" PreviewMouseLeftButtonDown="ImageDeleteSong_PreviewMouseLeftButtonDown"/>
<Image Name="ImageRemoveSong" Source="/Resources/Images/SpotWatch.Remove.png" Canvas.Left="697" Canvas.Top="6" Width="17" Height="19" MouseEnter="ImageRemoveSong_MouseEnter" MouseLeave="ImageRemoveSong_MouseLeave" PreviewMouseLeftButtonDown="ImageRemoveSong_PreviewMouseLeftButtonDown"/>
</Canvas>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The images do not need to be known in code behind, if you hook up the events you get the image control passed as the first argument. You just need to cast it.
Alternatively wrap the images in a button, bind the Command and pass something via the CommandParameter binding as needed. (Usually i avoid events and bind commands on view-models instead.)
Why do you need to access the images anyway? That's not something you should need to do. If you need to modify them you should bind the respective properties and then modify your bound data instead.
Given what you said in the comments, this is what i would do:
public SomeViewModel()
{
_deleteUser = new DelegateCommand(user =>
Users.Remove((Person)user)
);
}
private readonly ObservableCollection<Person> _Users;
public ObservableCollection<Person> Users { get { return _Users; } }
private readonly DelegateCommand _deleteUser;
public DelegateCommand DeleteUser { get { return _deleteUser; } }
<ItemsControl ItemsSource="{Binding Users}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- Some content here -->
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl},
Path=DataContext.DeleteUser}"
CommandParameter="{Binding}">Remove</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Key points:
Delete command at level of list.
Button binds to it via RelativeSource
Passes current item ({Binding}) as parameter.
Command casts parameter and removes it from list.
(DelegateCommand is a simple delegate based implementation of ICommand, you can find implementation examples on the web.)
I am building a tool and want to have a listview with parameters.
i have a base class called Parameter and many different derived classes, for example:
ParameterAddress, ParameterBool, ParameterString, ParameterScreen, ...
Each Parameter looks a bit different..
ParameterBool: Label + Checkbox
ParameterString: Label + Textbox
ParamterAddress: Label + Textbox + Button (for a new Dialog)
...
I have done this in my first try with an DataTemplateSelector.
Was nice, works "well"..
In my window i implemented the events like text changed, button clicked and so on..
but now i want to use this parameter view in another window too and dont want to copy the same text changed, button clicked events in each window again..
So my second try was to build in this into my parameter class.
Each Parameter class will have an Stackpanel and each derived class adds its Controls into it and can so handle whatever events just in one place!
But now in my listview only comes up the text "....Stackpanel" .
I think way 1 was a better direction, way 2 could work anyway ..
But what would be the "best" way?
EDIT
here are my actual datatemplates:
<Window.Resources>
<DataTemplate x:Key="textBoxTemplate">
<TextBox Text="{Binding Path=Value}" Width="100"></TextBox>
</DataTemplate>
<DataTemplate x:Key="checkBoxTemplate">
<CheckBox IsChecked="{Binding Path=Value}" IsThreeState="False"></CheckBox>
</DataTemplate>
<DataTemplate x:Key="screen_template">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Path=Value}" Width="100"></TextBox>
<Button Content="..." Click="btn_screenlist_Click" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="address_template">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Path=Value}" Width="100"></TextBox>
<Button Content="..." Click="btn_address_Click" />
</StackPanel>
</DataTemplate>
<local:ParameterTemplateSelector
x:Key="parameterTemplateSelector"
TextBoxTemplate="{StaticResource textBoxTemplate}"
CheckBoxTemplate="{StaticResource checkBoxTemplate}"
Screen_template="{StaticResource screen_template}"
Address_template="{StaticResource address_template}"
/>
</Window.Resources>
in my opinion its very unusable..
you have to maintain so many peaces of code to work..
create a new template
put it into the datatemplate resource
dont forget to put it into the datatemplateselector class
with having my "controls" directly in my class i have only this one peace of code.. ?!
You should make Datatemplates without keys pointing to your derives parameter class types. Wpf will automatically show the most suitable template for each class.
As for the textchanged or click-events, you should hang your "ontextchanged" or "isCheckedChanged) into the setter of the binding target (e.g. parameter.Text or parameter.IsTrue).
The OnClick of Buttons should be replaced with a proper command.
Whatever you did: don't instanciate controls in code behind ... thats the worst way to do it.
Okay, after some hours playing around, researching, reading and testing ..
Here is my actual solution, which works fine in multiple windows !
Please leave some comments, improvements, whatever .. Thanks.
I am having now a Parameter.cs and Parameter.xaml
Parameter.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:locale="clr-namespace:tests"
>
<DataTemplate DataType="{x:Type locale:ParameterString}">
<TextBox Text="{Binding Value}" MinWidth="100"></TextBox>
</DataTemplate>
<DataTemplate DataType="{x:Type locale:ParameterBool}">
<CheckBox IsChecked="{Binding Value}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type locale:ParameterAddress}">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}"/>
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" MinWidth="100"/>
<Button
Content="..."
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"
Command="{Binding EditAddrCmd}"
/>
</StackPanel>
</DataTemplate>
Snippet of Parameter.cs
public class ParameterAddress : Parameter
{
ControllerList controllers;
#region constructors
public ParameterAddress (ControllerList controllers, String address)
{
this.controllers=controllers;
Name="Address";
Value=address;
EditAddrCmd=new RelayCommand(ex => EditAddrCmdExec(), cex => EditAddrCmdCanExec());
}
// ..
#endregion // constructors
#region commands
public ICommand EditAddrCmd { get; internal set; }
private void EditAddrCmdExec ()
{
// open dialog to edit address and save to <Value>
}
private bool EditAddrCmdCanExec ()
{
return true;
}
#endregion // comannds
public override bool isValid ()
{
return true;
}
}
now i simply add to each window where i want to use these parameters the Parameter.xaml to its Resources:
SomeWindow.xaml
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Parameter.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
#SnowballTwo
did you mean this way of doing it?
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.