I implemented a tree model like this:
public class Node
{
public NodeValue Item { get; set; }
public Node Parent { get; set; }
public List<Node> Children { get; set; }
}
And I displayed this nodes in a WPF TreeView.
<TreeView Name="MainTreeview" HorizontalAlignment="Left" Height="auto" VerticalAlignment="Top" Width="200" ItemsSource="{Binding UpdateSourceTrigger=PropertyChanged}" SelectedItemChanged="MainTreeview_SelectedItemChanged" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local2:Node}" ItemsSource="{Binding Path=Children, Mode=OneWay}">
<TreeViewItem Header="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
My problem is that I cannot update view after I update the model. I don't understand from other articles how to update the UI, because they are too complex.
Now, I know that are 2 possible ways:
Using dependency properties: I understood how they work but I don't know how to apply them to my model.
Using Event triggers but I also don't know how to apply them to my problem.
Please help me!
Simply have your node implement INotifyPropertyChanged and use ObservableCollections instead of non-obserable Lists.
Partial implementation to get you on your way...
public class Node : INotifyPropertyChanged
{
private Node _parent;
public Node Parent
{
get
{
return this._parent;
}
set
{
if (value != this._parent)
{
this._parent= value;
NotifyPropertyChanged();
}
}
}
public ObservableCollection<Node> Children { get; } = new ObservableCollection<Node>()
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?(this, new PropertyChangedEventArgs(propertyName));
}
}
Related
Before you mark as duplicate, I have read the other SO entries, none seem to apply
I have two classes, one for menu item groups which will comprise my root menu items, and one for the sub items as such
public class RootItem
{
public string name {get;set;}
public image icon {get;set;}
public List<SubItem> subItems {get;set;}
}
public class SubItem
{
public string name {get;set;}
public image icon {get;set;}
}
The rest of what I am trying to do is probibly obvious but ill outline it anyway:
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<DataTemplate DataType="{x:Type root:RootItem}">
<DataTemplate.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="ItemsSource" Value="{Binding subItems}"/>
<Setter Property="Icon" Value="{Binding icon}"/>
</Style>
</DataTemplate.Resources>
</DataTemplate>
<DataTemplate DataType="{x:Type root:SubItem}">
<DataTemplate.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding name}"/>
<Setter Property="Icon" Value="{Binding icon}"/>
</Style>
</DataTemplate.Resources>
</DataTemplate>
</MenuItem.Resources>
</MenuItem>
This is just one of the methods I tried, in this one it doesn't seem like the style is used at all as only the root items show but they are blank, no icon or name.
I must be able to use 2 separate classes and the icon must be set-able and there must be 2 data templates or styles to handle/show the 2 classes differently.
I have also tried this, which while a mess, gets me 90% of the way, I just have issues where the icon is only visible on the last menu item.
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<imico:InputMapperIcon x:Key="ico" Icon="{Binding icon}" x:Shared="false"/>
<HierarchicalDataTemplate DataType="{x:Type root:RootItem}" x:Shared="false" ItemsSource="{Binding subItems}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{StaticResource ico}"/>
<Setter Property="MenuItem.Tag" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}, Path=DataContext}"/>
<EventSetter Event="MenuItem.Click" Handler="BtnAddCommand_Click"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</MenuItem.Resources>
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{StaticResource ico}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
here is a working example, copied from your second xaml snippet:
.xaml
<Grid Background="Transparent">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem ItemsSource="{Binding Path=RootItems}" Header="Menu">
<MenuItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type root:RootItem}" x:Shared="false" ItemsSource="{Binding subItems}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{Binding icon}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</MenuItem.Resources>
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding name}"/>
<Setter Property="MenuItem.Icon" Value="{Binding icon}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
.cs
public partial class MainWindow : Window
{
public ObservableCollection<RootItem> RootItems { get; set; }
public MainWindow()
{
InitializeComponent();
RootItems = new ObservableCollection<RootItem>();
var rootitem = new RootItem() { name = "rootitem" };
rootitem.subItems = new List<SubItem>();
var subitem = new SubItem() { name = "subitem" };
subitem.icon = new Image();
subitem.icon.Source = new BitmapImage(new Uri(#"C:\Users\username\source\repos\WpfApp3\WpfApp3\New Bitmap Image.bmp"));
var subitem2 = new SubItem() { name = "subitem2" };
subitem2.icon = new Image();
subitem2.icon.Source = new BitmapImage(new Uri(#"C:\Users\username\source\repos\WpfApp3\WpfApp3\New Bitmap Image.bmp"));
rootitem.subItems.Add(subitem);
rootitem.subItems.Add(subitem2);
RootItems.Add(rootitem);
this.DataContext = this;
}
}
public class RootItem
{
public string name { get; set; }
public Image icon { get; set; }
public List<SubItem> subItems { get; set; }
}
public class SubItem
{
public string name { get; set; }
public Image icon { get; set; }
}
so the mistake i imagine must be in how you assign image to your SubItem(which i removed in my example, i use a staticresource instead), or in your InputMapperIcon
Short version:
I am having a problem with the two-way binding of the IsSelected property of the ListBox container and the ListBox item, which causes unexpected behaviour in the appearance of datatemplated items, when changing their IsSelected property of items in my ViewModel. I am looking for help, since I don't understand what the problem is.
Long version:
I am creating a CustomControl using a ListBox. I am using a DataTemplate to style the objects in the ListBox.
DataTemplate:
<DataTemplate DataType="{x:Type substratePresenter:Target}">
<Ellipse Fill="{Binding MyColor}"
Width="{Binding Source={StaticResource WellSize}}"
Height="{Binding Source={StaticResource WellSize}}"
StrokeThickness="1.5"
Canvas.Left="{Binding Path=XPos}"
Canvas.Top="{Binding Path=YPos}"
ToolTip="{Binding Name}"
SnapsToDevicePixels="True"
Cursor="Hand">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<i:InvokeCommandAction Command="{Binding Path=MouseEnterCommand}"/>
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeave">
<i:InvokeCommandAction Command="{Binding Path=MouseLeaveCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Ellipse.Effect>
<!--THIS IS HACK SO THAT THE INITIAL STATE OF THE HOVEROVER SHADOW IS "OFF"-->
<DropShadowEffect Color="Blue" BlurRadius="10" ShadowDepth="0" Opacity="0" />
</Ellipse.Effect>
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Resources>
<!-- REF for using Storyboard animation, Glowon: http://stackoverflow.com/questions/1425380/how-to-animate-opacity-of-a-dropshadoweffect -->
<Storyboard x:Key="GlowOn">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(Effect).Opacity">
<SplineDoubleKeyFrame KeyTime="0:0:0.0" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="GlowOff">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(Effect).Opacity">
<SplineDoubleKeyFrame KeyTime="0:0:0.0" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Style.Resources>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Blue" BlurRadius="10" ShadowDepth="0" Opacity=".75" />
</Setter.Value>
</Setter>
<Setter Property="Stroke" Value="Black"/>
<Style.Triggers>
<!--Handel target target selection-->
<DataTrigger Binding="{Binding Path=IsSelected}" Value="True">
<Setter Property="Stroke" Value="White"/>
</DataTrigger>
<!--Handel target hovering-->
<!-- REF for using DataTrigger: https://msdn.microsoft.com/de-de/library/aa970679%28v=vs.90%29.aspx -->
<DataTrigger Binding="{Binding IsGroupHovered}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource GlowOn}"/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource GlowOff}"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</DataTemplate>
As you can see above, I am using the IsSelected property to change the color of the stroke from black to white, when an item IsSelected is true. To select an item and correspondingly change its appearance I am binding the IsSelected property in the ItemContainerStyle to the IsSelected property of my datatemplated items.
ListBox XAML:
<ListBox
x:Name="TargetListBox"
BorderThickness="0"
Width="{StaticResource LayoutGridWidthColumn1}"
Height="{StaticResource LayoutGridHeightRow1}"
ItemsSource="{Binding Path=TargetCollection}"
SelectionMode="Extended"
Grid.Column="1" Grid.Row="1"
Background="Transparent"
>
<i:Interaction.Behaviors>
<behavior:RubberBandBehavior />
</i:Interaction.Behaviors>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" Background="Transparent"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="MouseDoubleClick" Handler="listBoxItem_DoubleClick" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Canvas.Left"
Value="{Binding XPos, Converter={StaticResource horizontalValueConverter},
ConverterParameter={StaticResource substrateWidth}}"/>
<Setter Property="Canvas.Top"
Value="{Binding YPos, Converter={StaticResource verticalValueConverter},
ConverterParameter={StaticResource substrateHeight}}"/>
<!--Bind IsSelected property of ListBoxItem to that of the Target-->
<!--REF: http://stackoverflow.com/questions/1875450/binding-the-isselected-property-of-listboxitem-to-a-property-on-the-object-from-->
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
<!--<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}"/>-->
<!--Hide the background-highlighting of the ListBox-Selection, since we handle this from the Items-->
<!--REF: http://stackoverflow.com/questions/2138200/change-background-color-for-selected-listbox-item-->
</Style>
</ListBox.ItemContainerStyle>
<!--REF: http://stackoverflow.com/questions/4343793/how-to-disable-highlighting-on-listbox-but-keep-selection-->
<ListBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
</ListBox.Resources>
</ListBox>
I am now trying to implement double-click behaviour to select groups of identical items. I have this double-click event method in my code behind:
void listBoxItem_DoubleClick(object sender, MouseButtonEventArgs e)
{
(((ListBoxItem)sender).Content as Target).MouseSelectGroupCommand.Execute(null);
}
The command MouseSelectGroupCommand of Target finds the other Targets of the group in the ObservableCollection TargetCollection, which are identical to the selected one and sets their IsSelected property to true.
The problem I am now having, is that when I perform a double-click on a target, only that target changes its stroke color, but not the other targets of the group.
To try and debug I have done the following:
1) Confirm that the IsSelected property of all targets in the group are indeed set to true, which is the case.
2) I have changed the binding from <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/> to <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}/>" in <ListBox.ItemContainerStyle>. When I do this, it works and the stroke color changes for the whole group as expected. However I lose the selection behaviour of the ListBox, which I would then have to reimplement (such as deselection, when selecting another item, etc.). I would therefore like to avoid this.
Furthermore I am using precisely the same method to change the DropShadowEffect of the whole group, when a member-target of that group is being hovered (see DataTemplate) and in that case it works perfectly fine.
I am therefore left to conclude, that it somehow has to do with the binding of the IsSelected property. I would appreciate any suggestions on how to resolve this.
Update:
Here is the code that is executed by the MouseSelectGroupCommand. It sends a Message using MvvmLight Messenger to its containing collection, which the finds other targets that are identical and sets their IsSelected property to true. I know it is not pretty at all, but I am still very new to WPF it is what I have got working ATM. I would love to hear suggestions on how to handle this better, although that would be different question altogether.
MouseSelectGroupCommand executed on double-click:
public RelayCommand MouseSelectGroupCommand { get; set; }
private void ExecuteSelectTargetGroup()
{
List<Target> selectedTarget = new List<Target>();
selectedTarget.Add(this);
Messenger.Default.Send(new SelectTargetGroup(selectedTarget));
}
SelectGroup command, executed in the ObservableCollection containing the targets, when receiving the SelectTargetGroup message:
public void SelectGroup(IList<Target> selectedTarget)
{
IList<Target> targetGroup = GetTargetsWithSameActions(selectedTarget[0]);
SetGroupSelected(targetGroup);
}
public void SetGroupSelected(IList<Target> targetGroup)
{
foreach (Target target in targetGroup)
{
target.PropertyChanged -= TargetPropertyChanged;
target.IsSelected = true;
target.PropertyChanged += TargetPropertyChanged;
}
}
And this is how I have the command set up in the constructor of the ObservableCollection:
Messenger.Default.Register<SelectTargetGroup>(this, msg => SelectGroup(msg.SelectedTargets));
Update: It has become clear to me that the root of the problem is in my sloppy implementation. The answer Василий Шапенко should help me achieve a much cleaner implementation and therefore work around the problem, which is why I accepted it.
Ok, here is a small solution:
First is background part. In the code below we create a main view model, add property Items to it, and fill it with bunch of models. OnModelSelectionChanged does the work by selecting model groups.
public class MainViewModel
{
private ObservableCollection<SelectionItemViewModel> items;
public MainViewModel()
{
FillItems();
}
private void FillItems()
{
var models=Enumerable.Range(0, 10)
.SelectMany(
index =>
Enumerable.Range(0, 3)
.Select(i => new Model() {Id = index, Name = string.Format("Name_{0}_{1}", index, i)})).Select(
delegate(Model m)
{
var selectionItemViewModel = new SelectionItemViewModel()
{
Value = m
};
selectionItemViewModel.PropertyChanged += OnModelSelectionChanged;
return selectionItemViewModel;
});
Items=new ObservableCollection<SelectionItemViewModel>(models);
}
private void OnModelSelectionChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected")
{
var model = sender as SelectionItemViewModel;
foreach (var m in Items.Where(i=>i.Value.Id==model.Value.Id && model!=i))
{
if (m.IsSelected != model.IsSelected)// This one to prevent infinite loop on selection, on double click there is no need for it
{
m.IsSelected = model.IsSelected;
}
}
}
}
public ObservableCollection<SelectionItemViewModel> Items
{
get { return items; }
set { items = value; }
}
}
public class SelectionItemViewModel:INotifyPropertyChanged
{
private bool isSelected;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
public bool IsSelected
{
get { return isSelected; }
set { isSelected = value;
OnPropertyChanged();//For .Net<4.5, use OnPropertyChanged("IsSelected")
}
}
public Model Value { get; set; }
}
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
}
XAML. Here is simple binding, nothing complex.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox SelectionMode="Multiple" ItemsSource="{Binding Items}" DisplayMemberPath="Value.Name">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Window>
MainWindow.xaml.cs - here we put our ViewModel into MainWindow DataContext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
For double click support:
In MainWindow.xaml.cs:
private void Control_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var source = e.OriginalSource as FrameworkElement;
var mainViewModel = DataContext as MainViewModel;
if (source != null)
{
var model = source.DataContext as SelectionItemViewModel;
model.IsSelected = !model.IsSelected;
if (model != null)
{
foreach (var m in mainViewModel.Items.Where(i => i.Value.Id == model.Value.Id && model != i))
{
if (m.IsSelected != model.IsSelected)
{
m.IsSelected = model.IsSelected;
}
}
}
}
}
In MainWindow.xaml:
<ListBox MouseDoubleClick="Control_OnMouseDoubleClick" SelectionMode="Multiple" ItemsSource="{Binding Items}" DisplayMemberPath="Value.Name">
And comment the code inside OnModelSelection.
That is a direct rough approach. More elegant way is to create command binded to double click and attached to ListBoxItem, but this requires more code to write and understanding the concept of Attached Properties.
Also take a look at MouseButtonEventArgs, which will help you to determine which button is clicked, and which control key is pressed.
Key words for further readings: InputBinding,AttachedProperty,ICommand.
I have a TreeView intended to operate on two levels. For his I have two HierarchicalDataTemplate and two custom types. The ItemsSource is linked to a ObservableCollection and everything works fine. I just can't figure how to make a node selected or expanded from codebehind. Somewhere was mentioned a very good idea of binding IsExpanded and IsSelected properties to the corresponding properties in my custom types. The only problem is that the HierarchicalDataTemplate does not implement a TreeViewItem directly, so how can I access these properties in the following code?
<TreeView Name="treeViewNotes" AllowDrop="True" PreviewMouseLeftButtonDown="treeViewNotes_PreviewMouseLeftButtonDown" PreviewMouseMove="treeViewNotes_PreviewMouseMove" Drop="treeViewNotes_Drop" DragEnter="treeViewNotes_DragEnter">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type dataclasses:NoteFolder}" ItemsSource="{Binding Notes}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Source="{Binding TreeViewIcon}" Tag="{Binding Self}"/>
<TextBlock Text="{Binding Title}" Tag="{Binding Self}" Margin="3"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type dataclasses:Note}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Source="{Binding TreeViewIcon}" Tag="{Binding Self}"/>
<TextBlock Text="{Binding Title}" Tag="{Binding Self}" Margin="3"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
My goal is when creating and adding a new Note to some NoteFolder, to make this Note selected and the Folder expanded. The same would be needed to further improve the UI response on drag&drop.
You could try changing the TreeView's ItemContainerStyle in the following way, so that its IsExpanded and IsSelected properties would bind to the DataContext's IsExpanded and IsSelected:
<TreeView x:Name="..." ItemsSource="{Binding RootNode}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<!-- Items in the ItemsSource need to have these properties for the binding to work -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<!-- You can also optionally change some style values based on IsSelected and IsExpanded values -->
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="BorderThickness" Value="4 0 0 1"/>
<Setter Property="BorderBrush" Value="DeepSkyBlue"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="False">
<Setter Property="BorderThickness" Value="4 0 0 1 "/>
<Setter Property="BorderBrush" Value="Transparent"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate>
...
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Of course, then each item coming from the ItemSource and present in the hierarchy needs to have these properties.
The only problem is that the HierarchicalDataTemplate does not
implement a TreeViewItem directly, so how can I access these
properties in the following code?
You can do that in TreeView.ItemContainerStyle:
<TreeView ItemTemplate="{StaticResource ResourceKey=treeViewDataTemplate}"
ItemsSource="{Binding Data}"
Name="trvTreeView">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}" />
<EventSetter Event="Expanded" Handler="TreeViewItem_Expanded" />
<EventSetter Event="Collapsed" Handler="TreeViewItem_Collapsed" />
<EventSetter Event="PreviewMouseRightButtonDown" Handler="TreeViewItem_PreviewMouseRightButtonDown" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
In the code behind:
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem treeViewItem = e.OriginalSource as TreeViewItem;
if (treeViewItem != null)
{
BaseObjectExplorerNode baseObjectExplorerNode = treeViewItem.Header as BaseObjectExplorerNode;
if (baseObjectExplorerNode != null)
{
baseObjectExplorerNode.IsExpanded = true;
}
}
}
private void TreeViewItem_Collapsed(object sender, RoutedEventArgs e)
{
TreeViewItem treeViewItem = e.OriginalSource as TreeViewItem;
if (treeViewItem != null)
{
BaseObjectExplorerNode baseObjectExplorerNode = treeViewItem.Header as BaseObjectExplorerNode;
if (baseObjectExplorerNode != null)
{
baseObjectExplorerNode.IsExpanded = false;
}
}
}
And then for example:
root.IsExpanded = true;
I don't see any problem. You can make a StyleSelector, two Style's and bind to properties.
XAML:
<TreeView Name="treeViewNotes" AllowDrop="True" PreviewMouseLeftButtonDown="treeViewNotes_PreviewMouseLeftButtonDown" PreviewMouseMove="treeViewNotes_PreviewMouseMove" Drop="treeViewNotes_Drop" DragEnter="treeViewNotes_DragEnter">
<TreeView.Resources>
<!-- StyleSelector for containers -->
<notdataclasses:NoteStyleSelector x:Key="NoteStyleSelector" />
<!-- Style for a NoteFolder's container -->
<Style x:Key="NoteFolderStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}" />
<Setter Property="ItemContainerStyleSelector" Value="{StaticResource NoteFolderStyle}" />
</Style>
<!-- Style for a Note's container -->
<Style x:Key="NoteStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected}" />
</Style>
<!-- ... -->
</TreeView.Resources>
<TreeView.ItemContainerStyleSelector>
<StaticResource ResourceKey="NoteStyleSelector" />
</TreeView.ItemContainerStyleSelector>
</TreeView>
NoteStyleSelector:
public sealed class NoteStyleSelector : StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
FrameworkElement fe = container as FrameworkElement;
if (fe!= null)
{
if (item is Note)
{ return (Style)fe.FindResource("NoteStyle"); }
if (item is NoteFolder)
{ return (Style)fe.FindResource("NoteFolderStyle"); }
}
return base.SelectStyle(item, container);
}
}
In a drop handler:
currentFolder.Nodes.Add(pastedNode);
currentFolder.IsExpanded = true;
currentNode.IsSelected = true;
Data casses:
public class Note : INotifyPropertyChanged
{
// Only the IsSelected property because a Note can not be expanded
public bool IsSelected { get { /* ... */ } set { /* ... */ } }
}
public class NoteFolder : INotifyPropertyChanged
{
public bool IsSelected { get { /* ... */ } set { /* ... */ } }
public bool IsExpanded { get { /* ... */ } set { /* ... */ } }
}
I have a view model as follows:
public class SolutionViewModel : TreeViewItemViewModel {
public ObservableCollection<TreeViewItemViewModel> Children {
get { return mChildItems; }
}
public bool IsExpanded {
get { return mIsExpanded; }
set {
if (value != mIsExpanded) {
mIsExpanded = value;
OnPropertyChanged("IsExpanded");
}
if (mIsExpanded && mParentItem != null)
mParentItem.IsExpanded = true;
if (this.HasDummyChild) {
Children.Remove(EmptyItem);
LoadChildren();
}
}
}
public bool IsSelected {
get { return mIsSelected; }
set {
if (value != mIsSelected) {
mIsSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
protected override void LoadChildren() {
var subFolders = default(ReadOnlyCollection<Folder>);
if (!GetSubFolders(mSolution.Folder.Name, out subFolders)) {
subFolders = new ReadOnlyCollection<Folder>(new List<Folder>());
}
foreach (var folder in subFolders) {
Children.Add(new SolutionItemViewModel(this, folder));
}
}
public string SolutionName {
get { return mSolution.Name; }
}
}
The .Xaml for the TreeView is as follows:
<TreeView Name="SolutionTree" ItemsSource="{Binding SolutionViewModel}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type localmodels:SolutionViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="..\Resources\Folder_25x25.png" />
<TextBlock Text="{Binding SolutionName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
After a file is selected from SolutionExplorer:
public void SetBindingContext(SolutionViewModel SolutionViewModel) {
SolutionTree.DataContext = SolutionViewModel;
}
This is a lazy loading model so that when the item is expanded, the children are loaded.
The problem is that I am not even getting the Solution Name as the top level node.
Update:
I verified that the model has SolutionName assigned:
In addition, per comment from #elgonzo, I edited the .Xaml to reflect a change to ItemsSource binding:
<TreeView Name="SolutionTree" ItemsSource="{Binding}">
<TreeView.ItemContainerStyle>
Update 2
Code to assign the TreeView data context is executed when an event handler is raised after selecting a file from the OpenFileDialog:
private void OnOpenFile(string FilePath) {
mSolutionManager = SolutionManager.Load(FilePath);
mSolutionViewModel = new SolutionViewModel(mSolutionManager.Solution);
mMainWindow.SolutionExplorer.SetBindingContext(mSolutionViewModel);
mSolutionViewModel.Refresh();
}
When I step into the Refresh() method:
public void Refresh() {
OnPropertyChanged("SolutionName");
}
...I find that the PropertyChangedEventHandler has no subscribers.
Your binding to ItemsSource is wrong :
<TreeView Name="SolutionTree" ItemsSource="{Binding SolutionViewModel}">
ItemSource needs to be an IEnumerable.
I have the following snippet of code in an .xaml file:
<TreeView MouseDoubleClick="TreeView_MouseDoubleClick" ItemsSource="{Binding MyList}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
As you can see, when you "MouseDoubleClick" on an item in the TreeView it will execute the code in the code behind...namely...
private void TreeView_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
try
{
MessageBox.Show(((TreeViewWithViewModelDemo.LoadOnDemand.HtmlFileViewModel)(((System.Windows.Controls.TreeView)(sender)).SelectedValue)).HtmlFileName);
}
catch
{
}
}
I'm trying to follow the Model-View-ViewModel Design Pattern and would like to move the implementation of this MouseDoubleClick event away from the View and into the ViewModel.
I understand that if I was using a command I would use {Binding Command="Select"} (or something similar that implements the ICommand interface) but I cannot seem to find the syntax for this particular issue since it is not a command button.
Can someone help me out?
Thanks
Here's a solution using Blend's interaction triggers.
<Page.DataContext>
<Samples:TreeViewDblClickViewModel/>
</Page.DataContext>
<Grid>
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<DataTemplate>
<ContentControl>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding DoubleClickCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock Text="{Binding Name}" Background="AliceBlue" Margin="2"/>
</ContentControl>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
View model
public class TreeViewDblClickViewModel
{
public TreeViewDblClickViewModel()
{
Items = new List<TreeViewDblClickItem>
{
new TreeViewDblClickItem{ Name = "One"},
new TreeViewDblClickItem{ Name = "Two"},
new TreeViewDblClickItem{ Name = "Thee"},
new TreeViewDblClickItem{ Name = "Four"},
};
}
public IList<TreeViewDblClickItem> Items { get; private set; }
}
public class TreeViewDblClickItem
{
public TreeViewDblClickItem()
{
DoubleClickCommand = new ActionCommand(DoubleClick);
}
public string Name { get; set; }
private void DoubleClick()
{
Debug.WriteLine("Double click");
}
public ICommand DoubleClickCommand { get; private set; }
}
Using MVVM doesn't mean there mustn't be any code in the code-behind file. It just means moving all the associated logic into the viewmodel. You could just implement necessary double-click method on the viewmodel, and call it from the code behind like this:
_viewModel.MouseDoubleClickOnTree();
Also, I'd recommend looking at this topic: MVVM C# WPF binding mouse double click
I recommend you to start using library for MVVM pattern like Prism etc. It solves general problems and you can spend your time doing business stuff and not reinvent the wheel.
I actually posted an answer to someone about this very topic a few days ago. Here is what I posted
Obviously that is for a listviewitem, not a treeviewitem but it will still work, with some minor changes.