How to parse Children of TreeviewItem correctly in ViewModel? - c#

I have a TreeView control in my WPF program,which gets data from MySQL server and show databases and tables:
Server
...Databases
...Tables
What I want is when I click an item in the Treeview,I can get the Server/Databases/Tables' name.However,after hours testing,I still could not get the name.
HierarchicalDataTemplate of server
<HierarchicalDataTemplate
DataType="{x:Type local:ServerViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Figures/server.png" />
<TextBlock Text="{Binding ServerName}" />
</StackPanel>
</HierarchicalDataTemplate>
The other two are similar, except that HierarchicalDataTemplate of table does not have ItemsSource.
As I'm using MvvmLight pattern,I pass the SelectedItem as CommandParameterto ViewModel:
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<cmd:EventToCommand
Command="{Binding tableSelected}"
CommandParameter="{Binding SelectedItem,ElementName=MyTreeview}"
PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In the ViewModel,I delared a command to handle SelectedItemChanged event.I implemented the Execute() method as below,yet it did not work.Whenever I click a TreeviewItem,an unhandled exception System.Reflection.TargetInvocationException would be thrown.
private void GetSelectedItem(object parameter)
{
var item = parameter as TreeViewItem;
StackPanel stackpanel = (StackPanel)VisualTreeHelper.GetChild(item, 0);
TextBlock textblock = (TextBlock)VisualTreeHelper.GetChild(stackpanel, 1);
MessageBox.Show(textblock.Text);
}
Update
Command
public ICommand tableSelected { get; private set; }//In the ViewModel
tableSelected = new RelayCommand<object>((obj) => GetSelectedItem(obj), (obj) => true);
//Implemented in the ViewModel's constructor
I've read several related posts on Stackoverflow,like How I can get content of TreeViewItem in WPF?,WPF: Getting TreeViewItem's constituent controls,yet I still can not get it right.Please help,thanks.

I realized that what I was doing previously is totally wrong.The SelectedItem is not a collections of UI control,but purely an instance the same type as that of DataType.
<HierarchicalDataTemplate
DataType="{x:Type local:DatabaseViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Figures/database.png" />
<TextBlock Text="{Binding DatabaseName}" />
</StackPanel>
</HierarchicalDataTemplate>
The code above is part of my treeview.If I click a treeviewitem which uses this template,what I get is a DatabaseViewModel instance,rather than a StackPanel or its children.This is really misleading for WPF beginners.

Related

Is there a way to pass treeViewItem name as command parameter?

I am creating a simple treeview with binding to a collection. What I want is to display full info about the collection element in a listview when treeview element is selected. Similar to windows file explorer. The problem is I cannot bind selected treeviewitem to a Command.
Tried binding command to SelectedItemChanged event with a parameter of treeviewitem name - didn`t work.
Tried it with a simple event - gets cumbersome and breaks MVVM pattern. There must be an elegant way to solve this since what I am trying to do is really widespread in different apps.
<TreeView Grid.Row="1" Background="AliceBlue" ItemsSource="{Binding TopPanelNodes}" Name="TopTreeView" SourceUpdated="TopTreeView_SourceUpdated" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Nodes}">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" IsEnabled="False">
<TextBlock Text="{Binding Name}" >
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding UpdateInspectorCommand }" CommandParameter=??? />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
I would like to pass treeviewitem name as command parameter instead of ??? in my code so selected treeview item will be identified by name so I can get respective info and bind it to listview.
Solved it using RelativeSource.
CommandParameter="{Binding Path=SelectedItem, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeView}}}"
Works fine.

WPF Treeview how to select the parent node

I've seen many examples of how to perform this task (I sure have looked), but one line that seems to appear again and again is the following:
TreeViewItem item = e.OriginalSource as TreeViewItem;
Which makes sense. The problem is that, no matter what I click on in my treeview, item is always null.
My treeview has two layers. The top level is Scenes and the bottom level is characters. Each scene contains one more more characters. My aim is to be able to know the scene when the character is selected.
I suppose maybe my TreeView might be done differently to a lot of people's, so I shall provide that too:
<TreeView x:Name="ScenesTreeView01" Grid.Column="0" Width="Auto" Background="AliceBlue" ItemsSource="{Binding Scenes}" SelectedItemChanged="TreeView_SelectedItemChanged" BorderThickness="0">
<TreeView.DataContext>
<viewModels:ScenesViewModel />
</TreeView.DataContext>
<TreeView.Resources>
<ContextMenu x:Key="SceneLevel">
<MenuItem Header="Add selected character" Command="{Binding Path=DataContext.AddSelectedCharacter, Source={x:Reference ScenesTreeView01}}"/>
</ContextMenu>
<ContextMenu x:Key="CharacterLevel">
<MenuItem Header="Remove character from scene" Command="{Binding Path=DataContext.RemoveCharacterFromScene, Source={x:Reference ScenesTreeView01}}"/>
</ContextMenu>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Characters}">
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource SceneLevel}">
<TextBlock Text="{Binding SceneName}"></TextBlock>
<Image Source="{StaticResource ImgBook1}" Margin="0,0,5,0" Width="32" Height="32"/>
</StackPanel>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<Border BorderThickness="2" BorderBrush="LightBlue" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="2" CornerRadius="5,5,5,5">
<StackPanel Orientation="Horizontal" Margin="3" ContextMenu="{StaticResource CharacterLevel}">
<TextBlock FontFamily="Levenim MT" FontSize="16" VerticalAlignment="Center" MinWidth="50" Text="{Binding FirstName}"></TextBlock>
<Image Source="{Binding ImgIcon}" Margin="2" Width="32" Height="32"/>
</StackPanel>
</Border>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
e.OriginalSource does give something though, and that's this: System.Windows.Controls.TreeView Items.Count:4. I get that whether I select the top level or the bottom level (the top level has four items, the bottom levels have three each).
If someone could lend an eye to help me with this, that would be greatly appreciated. Thanks.
If you are binding, you won't get TreeViewItem any longer, but the collection items from your viewmodel/datacontext.
As far as the parent is concerned, I'd suggest to define it as a property of the above said viewmodel class.
I have a project/sample with a tree view on github
You could try to cast the NewValue property of the RoutedPropertyChangedEventArgs<object> to your Character type:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
Character c = e.NewValue as Character;
if (c != null)
{
//a character was selected...
}
}
This property should contain the newly selected item and if the cast succeeds you know that a Character was selected.
You should then be able to access a parent property or something of this class to get a reference to the parent scene.

Highlight custom DataTemplate in TreeView

I have a simple TreeView that I'm trying to create a custom DataTemplate for. It displays as desired, but when I attempt to select the TreeViewItem, the highlighting does not work if I click the text in the TreeView. However, if I select just to the left of the text, it works:
The source is pretty straight forward, so I'm guessing I'm just missing a styling connection:
xaml
<TreeView x:Name="treeView"
ItemsSource="{Binding TreeViewItems}"
Grid.Row="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<TreeViewItem Header="{Binding Header}">
<TreeViewItem.InputBindings>
<MouseBinding MouseAction="LeftClick"
Command="{Binding Command}" />
</TreeViewItem.InputBindings>
</TreeViewItem>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
I've tried adding this to the xaml, but it didnt help:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}"
BasedOn="{StaticResource {x:Type TreeViewItem}}" />
</TreeView.ItemContainerStyle>
TreeViewModel
public class TreeViewModel : BaseNotifyModel, ITreeViewModel
{
public TreeViewModel(ITreeViewService menuService)
{
TreeViewItems =
new ReadOnlyObservableCollection<MenuItemViewModel>(menuService.TreeViewMenu);
}
public ReadOnlyObservableCollection<MenuItemViewModel> TreeViewItems
{
get
{
return Get<ReadOnlyObservableCollection<MenuItemViewModel>>();
}
private set
{
Set(value);
}
}
}
MenuItemViewModel
public class MenuItemViewModel : BaseNotifyModel
{
public MenuItemViewModel()
{
MenuItems =
new ObservableCollection<MenuItemViewModel>();
}
public String Header
{
get
{
return Get<String>();
}
set
{
Set(value);
}
}
public ICommand Command
{
get
{
return Get<ICommand>();
}
set
{
Set(value);
}
}
public ObservableCollection<MenuItemViewModel> MenuItems
{
get
{
return Get<ObservableCollection<MenuItemViewModel>>();
}
set
{
Set(value);
}
}
}
The TreeView is creating a TreeViewItem for each item in ItemsSource, so don't nest another TreeViewItem inside the one that the TreeView already created for you. That doesn't serve any purpose. Your template should just be providing a way for the existing TreeViewItem to display whatever's in its DataContext (your MenuItemViewModel, in this case).
You want to display the Header property in the tree view item; so just do that. Nothing fancy, just a Label or ContentControl, or even a TextBlock if it's String (though it's a lot of fun in WPF to have the flexibility of arbitrary content). When the user clicks on the content, your command executes. The only thing the user can see in the tree is the content. That's the only visible part of the TreeViewItem item, so that's what the user is going to click on.
Problem number two: Once the input binding is getting LeftClick, that breaks selection in the TreeView. It appears to me that you can't get there from here by that method.
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<ContentControl
Content="{Binding Header}"
Background="Transparent"
>
<ContentControl.InputBindings>
<!-- This invokes the command, but breaks selection -->
<MouseBinding MouseAction="LeftClick"
Command="{Binding Command}" />
</ContentControl.InputBindings>
</ContentControl>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
Here's what you can do:
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<ContentControl
Content="{Binding Header}"
Background="Transparent"
>
</ContentControl>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<EventSetter Event="Selected" Handler="MenuTreeViewItem_Click" />
</Style>
</TreeView.ItemContainerStyle>
Codebehind
private void MenuTreeViewItem_Click(object sender, RoutedEventArgs e)
{
((MenuItemViewModel)((FrameworkElement)sender).DataContext).Command.Execute(null);
}
There is a way to bind an event to a command in pure XAML, but it requires some C# code (LOL). But I mean, it's "pure XAML" in the sense that it's a nice generalized reusable attached behavior, not an unsightly event handler in your codebehind. Instead, it does exactly what I did above, but it does it in code that you can more easily avert your eyes from, and that you can reuse in pure XAML.
With help from #Ed Plunket and #Evk, I found a solution that will work. I switched to a ContentPresenter and used the Interaction.Triggers to call the command on the MouseLeftButtonUp action.
<TreeView ItemsSource="{Binding TreeViewItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<ContentPresenter Content="{Binding Header}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding Path=DataContext.Command, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ContentPresenter>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

Method in ContextMenu for (Hierarchical)DataTemplate calls TreeViewItem's method instead of the one in ViewModel

I have a TreeView with multiple HierarchicalDataTemplate & DataTemplate items and I'm using Caliburn Micro for mvvm. The ItemsSource for the treeview is pointing to a collection in the viewmodel called 'TreeData' and I tried adding a specific ContextMenu for each HierarchicalDataTemplate & DataTemplate.
In the ContextMenu I use the caliburn functionality "cal:Message.Attach" to call a function in the
I made a smaller example of the treeview to illustrate the problem.
In the ViewModel (the collection object):
public class MyViewModel
{
// TreeData object
public ObservableCollection<TestRoot> TreeData = new ObservableCollection<TestRoot>()
{
new TestRoot()
{
Name = "Root item"
}
};
// the function on the viewmodel that should be called
public void DoSomething(object item)
{
MessageBox.Show("MyViewModel - DoSomething called");
}
}
The collection object:
public class TestRoot
{
public string Name { get; set; }
// caliburn calls this instead of the one on the viewmodel
public void DoSomething(object item)
{
MessageBox.Show("TestRoot - DoSomething called");
}
}
MyView.xaml treeview with only one (Hierarchical)DataTemplate:
<TreeView Margin="5" ItemsSource="{Binding TreeData}">
<TreeView.Resources>
<DataTemplate DataType="{x:Type vm:TestRoot}" >
<StackPanel Orientation="Horizontal">
<StackPanel.ContextMenu>
<ContextMenu>
<!-- caliburn (?) chooses the method on the collection object, not the viewmodel -->
<MenuItem Header="test dosomething" cal:Message.Attach="DoSomething($dataContext)"></MenuItem>
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
In another piece of code, I placed the ContextMenu in the TreeView.ContextMenu. There it worked as it 'should', pointing to the method on the viewmodel.
Looking around for a solution, I see things like "inheritance context". I think it might have something to do with it, but I'm not sure. How can I tell caliburn it must look in the viewmodel for my method, instead of the item in the TreeView I clicked on?
Or is there another possibility? For example: defining the different ContextMenus in the Resources and pointing them to the DataTemplates? But, wont that cause the exact same problem?
Please note that I'd like to keep the code-behind as minimal as possible. Thanks
update
For the completeness, here's the real development code. This should be right, no?
<TreeView ItemsSource="{Binding OrderTreeViewData.OrderTreeViewCategories}"
cal:Message.Attach="[Event SelectedItemChanged] = [Action OnSelectedItemChanged($this)]">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<!-- We have to select the item which is right-clicked on -->
<EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown"
Handler="TreeViewItem_PreviewMouseRightButtonDown"/>
<!-- set expanded -->
<Setter Property="TreeViewItem.IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<!-- dredge nodes -->
<HierarchicalDataTemplate DataType="{x:Type programs:DredgeRoot}"
ItemsSource="{Binding Dredgezones}">
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Add dredge zone" cal:Message.Attach="TreeViewAddDredgeZone($datacontext)"></MenuItem>
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
<!-- omitted other templates -->
</TreeView.Resources>
</TreeView>
Unfortunately there is still one tricky part to deal with. Due to the specific Popup behavior it doesn't inherit DataContext. To access proper context you have to get the PlacementTarget:
<StackPanel Tag="{Binding DataContext, RelativeSource={RelativeSource Self}}">
<StackPanel.ContextMenu>
<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<MenuItem Header="test dosomething" cal:Message.Attach="DoSomething"/>
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>
I think you have the nearly same problem as me, maybe see this topic : Bind contextMenu to a different viewmodel from treeview
You can try to use a command :
Try to change you code to :
<ContextMenu x:Key="MyContextMenu">
<MenuItem Header="Add dredge zone" Command="{Binding PlacementTarget.Tag.DataContext.TreeViewAddDredgeZoneCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"
CommandParameter="{Binding}"></MenuItem>
</ContextMenu>
Then add to your hierarchicalDataTemplate a Tag and ContextMenu
<HierarchicalDataTemplate DataType="{x:Type programs:DredgeRoot}"
ItemsSource="{Binding Dredgezones}" Tag="{Binding RelativeSource={RelativeSource AncestorType=UserControl}} ContextMenu="{StaticResource MyContextMenu}">
And in your viewmodel you can add your command with something like this :
public ICommand TreeViewAddDredgeZoneCommand
{
//your code here
}

ComboBox in a ListBox does not fire SelectionChanged event

I have a wpf, mvvm app, using the catel (http://catel.codeplex.com) framework\toolkit, C# .Net 4.0. The app has a ListBox with a TextBlock and ComboBox. The ListBox and ComboBox are populated from 2 different ObservableCollection from the ViewModel. I need to save (to a db), when the user clicks a button, each row in the ListBox where the user has selected an item from the ComboBox. The SelectionChanged event does not fire for any of the ComboBoxes in the ListBox. The idea being that I add to a list (ArrayList or IList?), in the ViewModel, each time the user selects an item in a ComboBox and for what row the item has been selected.
Or am I going about this the wrong way by trying to use the ComboBoxe SelectionChanged event? I also tried iterating thru the ListBox.Items but this seems like a hak and I want to avoid ui element logic in the ViewModel if possible.
The xaml:
<Grid>
<StackPanel Orientation="Horizontal">
<Label Width="180">Field1</Label>
<ListBox Height="200"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding List1, Mode=OneWay}"
Name="listBox1"
SelectionMode="Single"
Width="300">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Width="290">
<TextBlock Width="90" Text="{Binding}"></TextBlock>
<ComboBox Width="180" ItemsSource="{Binding DataContext.List2, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" DisplayMemberPath="Field1">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<catel:EventToCommand Command="{Binding SelectionChangedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" DisableAssociatedObjectOnCannotExecute="False" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
ViewModel code:
//in the ViewModel constructor
SelectionChangedCommand = new Command<SelectionChangedEventArgs>(OnSelectionChangedCommandExecute, OnSelectionChangedCommandCanExecute);
public Command<SelectionChangedEventArgs> SelectionChangedCommand { get; private set; }
private bool OnSelectionChangedCommandCanExecute()
{
return true;
}
private void OnSelectionChangedCommandExecute(SelectionChangedEventArgs e)
{
// add or update list....
}
In Command binding you have used binding which has relative source binding...
consider making these changes in binding
1) using list box as Ancestortype
2) While binding use Path=DataContext.SelectionChangedCommand otherwise it will take list box as datacontext.
<catel:EventToCommand Command="{Binding Path=DataContext.SelectionChangedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}" DisableAssociatedObjectOnCannotExecute="False" PassEventArgsToCommand="True" />

Categories