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.
Related
Following on from a previous question here, I'm struggling to style a MenuItem's Icon with a control I have that inserts icon images based upon a string dependency property.
Initially I started with:
<ContextMenu ItemsSource="{Binding MenuItems}">
<ContextMenu.Resources>
<Style TargetType="MenuItem">
...
<Setter Property="Icon">
<local:StringToIcon IconName="{Binding IconName}" />
</Setter>
</Style>
</ContextMenu.Resources>
</ContextMenu>
This had the predictable effect of only displaying one of the icons in the menu, usually the last one, as the instance was shared around.
I then tried the non-shared resource approach:
<ContextMenu ItemsSource="{Binding MenuItems}">
<ContextMenu.Resources>
<local:StringToIcon x:Key="MenuIcon" x:Shared="False" IconName="{Binding IconName}" />
<Style TargetType="MenuItem">
...
<Setter Property="Icon" Value="{StaticResource MenuIcon} />
</Style>
</ContextMenu.Resources>
</ContextMenu>
This had no effect. It didn't offer me x:Shared in Intellisense, so I wonder if that's an invalid property here.
Out of desperation, I threw the thing into a template:
<Setter Property="Icon">
<Setter.Value>
<ContentControl>
<ContentControl.Template>
<ControlTemplate>
<local:StringToIcon IconName="{Binding IconName}" />
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
</Setter.Value>
</Setter>
Again, no effect. My StringToIcon looks like this at the moment, hard-coded with a single image to check the problem doesn't lie there. (Or does it?)
<UserControl x:Class="RAP.Admin3.Components.StringToIcon"
...
>
<Image DataContext="{Binding ElementName=StringIconControl}" Source="pack://application:,,,/Resources/Icons/lorry.png"/>
</UserControl>
How do I get this darn thing to template and allow multiple uses? It's probably something basic I'm overlooking.
I've looked at various similar questions, and most seem to have success with the non-shared resource method.
Edit: Let me add substantially more code as requested. I've come up with a minimal replication of the problem:
The context menu is part of a TreeView resource.
<UserControl x:Class="MyApp.ItemHierarchy"
...
Name="ItemHierarchyControl">
<Grid>
<TreeView ItemsSource="{Binding ElementName=ItemHierarchyControl, Path=Items}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:HierarchyItem}" ItemsSource="{Binding Subitems}">
<StackPanel Orientation="Horizontal" Margin="0,1,4,1">
<TextBlock Text="My text" VerticalAlignment="Center" />
<StackPanel.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuItems}">
<ContextMenu.Resources>
<local:StringToIcon x:Key="MenuIcon" x:Shared="False" IconName="{Binding IconName}" />
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Path=Name}" />
<Setter Property="Icon" Value="{StaticResource MenuIcon}" />
</Style>
</ContextMenu.Resources>
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</UserControl>
This is backed by a dependency property for the items.
public ObservableCollection<HierarchyItem> Items
{
get { return (ObservableCollection<HierarchyItem>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ObservableCollection<HierarchyItem>), typeof(ItemHierarchy), new PropertyMetadata(new ObservableCollection<HierarchyItem>()));
StringToIcon is also backed by a string dependency property for the icon name, which is summarily ignored because of the hard-coded image at the moment.
HierarchyItems are simple for the example:
public ObservableCollection<HierarchyItem> Subitems { get; set; }
public ObservableCollection<BindableMenuItem> MenuItems { get; set; }
Just to get this proof working, I attached the ItemHierarchy to some properties of the main window:
public ObservableCollection<BindableMenuItem> MenuItems { get; set; }
public ObservableCollection<HierarchyItem> IHItems { get; set; }
public MainWindow()
{
MenuItems = new ObservableCollection<BindableMenuItem>();
MenuItems.Add(new BindableMenuItem("Item", null));
MenuItems.Add(new BindableMenuItem("Item", null));
MenuItems.Add(new BindableMenuItem("Item", null));
MenuItems.Add(new BindableMenuItem("Item", null));
IHItems = new ObservableCollection<HierarchyItem>();
IHItems.Add(new HierarchyItem() { MenuItems = this.MenuItems });
InitializeComponent();
}
Edit 2: Here's BindableMenuItem also:
public class BindableMenuItem
{
public BindableMenuItem(string name, ICommand command)
{
this.Name = name;
this.Command = command;
}
public string Name { get; set; }
public ICommand Command { get; set; }
public string IconName { get; set; }
public ObservableCollection<BindableMenuItem> Children { get; set; }
}
Try to move the StringToIcon to <TreeView.Resources>:
<TreeView ItemsSource="{Binding ElementName=ItemHierarchyControl, Path=Items}">
<TreeView.Resources>
<local:StringToIcon x:Key="MenuIcon" x:Shared="False" IconName="{Binding IconName}" />
<HierarchicalDataTemplate DataType="{x:Type local:HierarchyItem}" ItemsSource="{Binding Subitems}">
<StackPanel Orientation="Horizontal" Margin="0,1,4,1">
<TextBlock Text="My text" VerticalAlignment="Center" />
<StackPanel.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuItems}">
<ContextMenu.Resources>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Path=Name}" />
<Setter Property="Icon" Value="{StaticResource MenuIcon}" />
</Style>
</ContextMenu.Resources>
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
I'm trying to use the Flyouts from MahApps.Metro in my application. So I added this part to my MainWindow.xaml:
<controls:MetroWindow.Flyouts>
<controls:FlyoutsControl ItemsSource="{Binding Flyouts}">
<controls:FlyoutsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SettingsViewModel}">
<view:SettingsFlyout/>
</DataTemplate>
</controls:FlyoutsControl.ItemTemplate>
</controls:FlyoutsControl>
</controls:MetroWindow.Flyouts>
The ItemTemplate will contain the mappings from my viewmodels to the views. Flyouts is an ObservableCollection<IFlyoutViewModel> and currently only contains my SettingsViewModel.
The IFlyoutViewModel definition:
using System.ComponentModel;
namespace MyApplication.ViewModel
{
internal interface IFlyoutViewModel : INotifyPropertyChanged
{
bool Visible { get; set; }
}
}
And how I use the Visible-property:
<controls:Flyout x:Class="MyApplication.View.SettingsFlyout"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
Header="Settings"
Position="Right"
IsOpen="{Binding Visible}"
Width="300">
...
</controls:Flyout>
So now I set the Visible-property of my SettingsViewModel, but the Flyout won't open. What am I doing wrong?
I just tried to assign IsOpen="true" hardcoded but this didn't work, too. So displaying the flyout with a datatemplate seems to be the problem...
I built it like described in the issue dicussion linked by Eldho, now it works. The key ist to define ItemContainerStyle and bind IsOpen there!
The new MainWindow.xaml:
<controls:MetroWindow.Flyouts>
<controls:FlyoutsControl ItemsSource="{Binding Flyouts}">
<controls:FlyoutsControl.Resources>
<view:FlyoutPositionConverter x:Key="FlyoutPositionConverter"/>
</controls:FlyoutsControl.Resources>
<controls:FlyoutsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:SettingsViewModel}">
<view:SettingsFlyout/>
</DataTemplate>
</controls:FlyoutsControl.ItemTemplate>
<controls:FlyoutsControl.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type controls:Flyout}}"
TargetType="{x:Type controls:Flyout}">
<Setter Property="Header"
Value="{Binding Header}" />
<Setter Property="IsOpen"
Value="{Binding Visible}" />
<Setter Property="Position"
Value="{Binding Position, Converter={StaticResource FlyoutPositionConverter}}" />
<Setter Property="IsModal"
Value="{Binding IsModal}" />
<Setter Property="Theme" Value="Accent" />
</Style>
</controls:FlyoutsControl.ItemContainerStyle>
</controls:FlyoutsControl>
</controls:MetroWindow.Flyouts>
The new IFlyoutViewModel:
using System.ComponentModel;
namespace MyApplication.ViewModel
{
internal interface IFlyoutViewModel : INotifyPropertyChanged
{
string Header { get; }
bool Visible { get; set; }
Position Position { get; set; }
bool IsModal { get; set; }
}
public enum Position
{
Top,
Left,
Right,
Bottom
}
}
The FlyoutPositionConverter is just a mapper between my position enum and the MahApps.Metro.Controls.Position because I didn't want to use the real positon in my viewmodel interface.
Also the view now no longer needs to be a Flyout, it can be a normal usercontrol.
Your first solution also should work well if you call OnPropertyChanged on the Setter of your Visible-Property
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 question about a listview setup like the example below. When I click the button below in the expander header I want that item to be selected as well, but what I'm seeing is while the button command does work, the item selected is still the previous item selected, not the item my button is in. How can I have the Item selected when the button is clicked?
I tried setting up a ControlTemplate like this, but it did not work.
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ListView ItemsSource="{Binding MyItemSource,
Mode=TwoWay}"
SelectedItem="{Binding MySelectedItem,
Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Expander IsExpanded="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}}, Path=IsSelected}">
<Expander.Header>
<Button Command={Binding MyCommand}>Click Me</Button>
</Expander.Header>
<!-- content here -->
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I would suggest defining a command SelectItem in the main ViewModel which takes the item which is to be selected as a parameter. The execution method of this command can then set the MySelectedItem property, set a property IsSelected on the item ViewModel to true and invoke all further actions on the item itself (i.e. what is now executed by MyCommand). With the selection logic in the ViewModel and a clean binding you don't even need to use ListView at all but can stick to a plain ItemsControl:
The XAML then looks like this:
<ItemsControl ItemsSource="{Binding MyItemSource}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander IsExpanded="{Binding IsSelected}">
<Expander.Header>
<Button Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ItemsControl}, Path=DataContext.SelectItem}"
CommandParameter="{Binding"}>Click Me</Button>
</Expander.Header>
<!-- content here -->
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The MainViewModel would look something like this:
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<ItemViewModel> MyItemsSource { get; private set; }
public ItemViewModel SelectedItem { get... set... }
public ICommand SelectItem { get; private set; }
ctor()...
private void ExecuteSelectItem(ItemViewModel item)
{
SelectedItem = item;
foreach (var i in MyItemsSource) i.IsSelected = false;
item.IsSelected = true;
item.DoSomething();
}
}
I have always found it way easier to use ItemsControl an implement the few lines of selection logic myself, instead of dealing with the messy binding of the selection of a ListView. In my opinion it is a quite intuitive to implement custom selection behavior (multiple items, allowing only certain combinations, etc.). You can use the IsSelected property easily to apply a custom styling of selected items.
You can try something like this in your view model, add if statement in setter:
private object _mySelectedItem;
public object MySelectedItem
{
get { return _mySelectedItem; }
set
{
if (_mySelectedItem != value && value != null)
{
_mySelectedItem = value;
OnPropertyChanged("MySelectedItem");
}
}
}
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));
}
}