Given the following code:
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" />
<MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2"/>
<MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3"/>
</MenuItem>
In XAML, is there a way to create checkable menuitem's that are mutually exclusive? Where is the user checks item2, item's 1 and 3 are automatically unchecked.
I can accomplish this in the code behind by monitoring the click events on the menu, determining which item was checked, and unchecking the other menuitems. I'm thinking there is an easier way.
Any ideas?
This may not be what you're looking for, but you could write an extension for the MenuItem class that allows you to use something like the GroupName property of the RadioButton class. I slightly modified this handy example for similarly extending ToggleButton controls and reworked it a little for your situation and came up with this:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace WpfTest
{
public class MenuItemExtensions : DependencyObject
{
public static Dictionary<MenuItem, String> ElementToGroupNames = new Dictionary<MenuItem, String>();
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName",
typeof(String),
typeof(MenuItemExtensions),
new PropertyMetadata(String.Empty, OnGroupNameChanged));
public static void SetGroupName(MenuItem element, String value)
{
element.SetValue(GroupNameProperty, value);
}
public static String GetGroupName(MenuItem element)
{
return element.GetValue(GroupNameProperty).ToString();
}
private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Add an entry to the group name collection
var menuItem = d as MenuItem;
if (menuItem != null)
{
String newGroupName = e.NewValue.ToString();
String oldGroupName = e.OldValue.ToString();
if (String.IsNullOrEmpty(newGroupName))
{
//Removing the toggle button from grouping
RemoveCheckboxFromGrouping(menuItem);
}
else
{
//Switching to a new group
if (newGroupName != oldGroupName)
{
if (!String.IsNullOrEmpty(oldGroupName))
{
//Remove the old group mapping
RemoveCheckboxFromGrouping(menuItem);
}
ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
menuItem.Checked += MenuItemChecked;
}
}
}
}
private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
{
ElementToGroupNames.Remove(checkBox);
checkBox.Checked -= MenuItemChecked;
}
static void MenuItemChecked(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
foreach (var item in ElementToGroupNames)
{
if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
{
item.Key.IsChecked = false;
}
}
}
}
}
Then, in the XAML, you'd write:
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item1" />
<MenuItem x:Name="MenuItem_Item2" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item2"/>
<MenuItem x:Name="MenuItem_Item3" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item3"/>
</MenuItem>
It's a bit of a pain, but it offers the perk of not forcing you to write any additional procedural code (aside from the extension class, of course) to implement it.
Credit goes to Brad Cunningham who authored the original ToggleButton solution.
Adding this at the bottom since I don't have the reputation yet...
As helpful as Patrick's answer is, it doesn't ensure that items cannot be unchecked. In order to do that, the Checked handler should be changed to a Click handler, and changed to the following:
static void MenuItemClicked(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
if (menuItem.IsChecked)
{
foreach (var item in ElementToGroupNames)
{
if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
{
item.Key.IsChecked = false;
}
}
}
else // it's not possible for the user to deselect an item
{
menuItem.IsChecked = true;
}
}
You can also use a Behavior. Like this one:
<MenuItem Header="menu">
<MenuItem x:Name="item1" Header="item1" IsCheckable="true" ></MenuItem>
<MenuItem x:Name="item2" Header="item2" IsCheckable="true"></MenuItem>
<MenuItem x:Name="item3" Header="item3" IsCheckable="true" ></MenuItem>
<i:Interaction.Behaviors>
<local:MenuItemButtonGroupBehavior></local:MenuItemButtonGroupBehavior>
</i:Interaction.Behaviors>
</MenuItem>
public class MenuItemButtonGroupBehavior : Behavior<MenuItem>
{
protected override void OnAttached()
{
base.OnAttached();
GetCheckableSubMenuItems(AssociatedObject)
.ToList()
.ForEach(item => item.Click += OnClick);
}
protected override void OnDetaching()
{
base.OnDetaching();
GetCheckableSubMenuItems(AssociatedObject)
.ToList()
.ForEach(item => item.Click -= OnClick);
}
private static IEnumerable<MenuItem> GetCheckableSubMenuItems(ItemsControl menuItem)
{
var itemCollection = menuItem.Items;
return itemCollection.OfType<MenuItem>().Where(menuItemCandidate => menuItemCandidate.IsCheckable);
}
private void OnClick(object sender, RoutedEventArgs routedEventArgs)
{
var menuItem = (MenuItem)sender;
if (!menuItem.IsChecked)
{
menuItem.IsChecked = true;
return;
}
GetCheckableSubMenuItems(AssociatedObject)
.Where(item => item != menuItem)
.ToList()
.ForEach(item => item.IsChecked = false);
}
}
Since there is not a samilar answer, I post my solution here:
public class RadioMenuItem : MenuItem
{
public string GroupName { get; set; }
protected override void OnClick()
{
var ic = Parent as ItemsControl;
if (null != ic)
{
var rmi = ic.Items.OfType<RadioMenuItem>().FirstOrDefault(i =>
i.GroupName == GroupName && i.IsChecked);
if (null != rmi) rmi.IsChecked = false;
IsChecked = true;
}
base.OnClick();
}
}
In XAML just use it as an usual MenuItem:
<MenuItem Header="OOO">
<local:RadioMenuItem Header="111" GroupName="G1"/>
<local:RadioMenuItem Header="222" GroupName="G1"/>
<local:RadioMenuItem Header="333" GroupName="G1"/>
<local:RadioMenuItem Header="444" GroupName="G1"/>
<local:RadioMenuItem Header="555" GroupName="G1"/>
<local:RadioMenuItem Header="666" GroupName="G1"/>
<Separator/>
<local:RadioMenuItem Header="111" GroupName="G2"/>
<local:RadioMenuItem Header="222" GroupName="G2"/>
<local:RadioMenuItem Header="333" GroupName="G2"/>
<local:RadioMenuItem Header="444" GroupName="G2"/>
<local:RadioMenuItem Header="555" GroupName="G2"/>
<local:RadioMenuItem Header="666" GroupName="G2"/>
</MenuItem>
Quite simple and clean. And of course you can make the GroupName a dependency property by some additional codes, that's all the same as others.
BTW, if you do not like the check mark, you can change it to whatever you like:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var p = GetTemplateChild("Glyph") as Path;
if (null == p) return;
var x = p.Width/2;
var y = p.Height/2;
var r = Math.Min(x, y) - 1;
var e = new EllipseGeometry(new Point(x,y), r, r);
// this is just a flattened dot, of course you can draw
// something else, e.g. a star? ;)
p.Data = e.GetFlattenedPathGeometry();
}
If you used plenty of this RadioMenuItem in your program, there is another more efficient version shown below. The literal data is aquired from e.GetFlattenedPathGeometry().ToString() in previous code snippet.
private static readonly Geometry RadioDot = Geometry.Parse("M9,5.5L8.7,7.1 7.8,8.3 6.6,9.2L5,9.5L3.4,9.2 2.2,8.3 1.3,7.1L1,5.5L1.3,3.9 2.2,2.7 3.4,1.8L5,1.5L6.6,1.8 7.8,2.7 8.7,3.9L9,5.5z");
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var p = GetTemplateChild("Glyph") as Path;
if (null == p) return;
p.Data = RadioDot;
}
And at last, if you plan to wrap it for use in your project, you should hide IsCheckable property from the base class, since the auto check machenism of MenuItem class will lead the radio check state mark a wrong behavior.
private new bool IsCheckable { get; }
Thus VS will give an error if a newbie try to compile XAML like this:
// note that this is a wrong usage!
<local:RadioMenuItem Header="111" GroupName="G1" IsCheckable="True"/>
// note that this is a wrong usage!
Yes, this can be done easily by making every MenuItem a RadioButton. This can be done by Editing Template of MenuItem.
Right-Click the MenuItem in the Document-Outline left pane > EditTemplate > EditCopy. This will add the code for editing under Window.Resources.
Now, you have to do only two-changes which are very simple.
a. Add the RadioButton with some Resources to hide its circle portion.
b. Change BorderThickness = 0 for MenuItem Border part.
These changes are shown below as comments, rest of the generated style should be used as is :
<Window.Resources>
<LinearGradientBrush x:Key="MenuItemSelectionFill" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#34C5EBFF" Offset="0"/>
<GradientStop Color="#3481D8FF" Offset="1"/>
</LinearGradientBrush>
<Geometry x:Key="Checkmark">M 0,5.1 L 1.7,5.2 L 3.4,7.1 L 8,0.4 L 9.2,0 L 3.3,10.8 Z</Geometry>
<ControlTemplate x:Key="{ComponentResourceKey ResourceId=SubmenuItemTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}" TargetType="{x:Type MenuItem}">
<Grid SnapsToDevicePixels="true">
<Rectangle x:Name="Bg" Fill="{TemplateBinding Background}" RadiusY="2" RadiusX="2" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"/>
<Rectangle x:Name="InnerBorder" Margin="1" RadiusY="2" RadiusX="2"/>
<!-- Add RadioButton around the Grid
-->
<RadioButton Background="Transparent" GroupName="MENUITEM_GRP" IsHitTestVisible="False" IsChecked="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=MenuItem}}">
<RadioButton.Resources>
<Style TargetType="Themes:BulletChrome">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</RadioButton.Resources>
<!-- Add RadioButton Top part ends here
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="24" SharedSizeGroup="MenuItemIconColumnGroup" Width="Auto"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="37"/>
<ColumnDefinition SharedSizeGroup="MenuItemIGTColumnGroup" Width="Auto"/>
<ColumnDefinition Width="17"/>
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon" ContentSource="Icon" Margin="1" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
<!-- Change border thickness to 0
-->
<Border x:Name="GlyphPanel" BorderBrush="#CDD3E6" BorderThickness="0" Background="#E6EFF4" CornerRadius="3" Height="22" Margin="1" Visibility="Hidden" Width="22">
<Path x:Name="Glyph" Data="{StaticResource Checkmark}" Fill="#0C12A1" FlowDirection="LeftToRight" Height="11" Width="9"/>
</Border>
<ContentPresenter Grid.Column="2" ContentSource="Header" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<TextBlock Grid.Column="4" Margin="{TemplateBinding Padding}" Text="{TemplateBinding InputGestureText}"/>
</Grid>
</RadioButton>
<!-- RadioButton closed , thats it !
-->
</Grid>
...
</Window.Resources>
Apply the Style ,
<MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
I just thought I would throw in my solution, since none of the answers met my needs. My full solution is here...
WPF MenuItem as a RadioButton
However, the basic idea is to use ItemContainerStyle.
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
<EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
</Style>
</MenuItem.ItemContainerStyle>
And the following event click should be added so that the RadioButton is checked when the MenuItem is clicked (otherwise you have to click exactly on the RadioButton):
private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e)
{
MenuItem mi = sender as MenuItem;
if (mi != null)
{
RadioButton rb = mi.Icon as RadioButton;
if (rb != null)
{
rb.IsChecked = true;
}
}
}
Here's a simple, MVVM-based solution that leverages a simple IValueConverter and CommandParameter per MenuItem.
No need to re-style any MenuItem as a different type of control. MenuItems will automatically be deselected when the bound value doesn't match the CommandParameter.
Bind to an int property (MenuSelection) on the DataContext (ViewModel).
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" IsChecked="{Binding MenuSelection, ConverterParameter=1, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
<MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2" IsChecked="{Binding MenuSelection, ConverterParameter=2, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
<MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3" IsChecked="{Binding MenuSelection, ConverterParameter=3, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
</MenuItem>
Define your value converter. This will check the bound value against the command parameter and vice versa.
public class MatchingIntToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var paramVal = parameter as string;
var objVal = ((int)value).ToString();
return paramVal == objVal;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool)
{
var i = System.Convert.ToInt32((parameter ?? "0") as string);
return ((bool)value)
? System.Convert.ChangeType(i, targetType)
: 0;
}
return 0; // Returning a zero provides a case where none of the menuitems appear checked
}
}
Add your resource
<Window.Resources>
<ResourceDictionary>
<local:MatchingIntToBooleanConverter x:Key="MatchingIntToBooleanConverter"/>
</ResourceDictionary>
</Window.Resources>
Good luck!
There is not a built-in way to do this in XAML, you will need to roll your own solution or get an existing solution if available.
I achieved this using a couple of lines of code:
First declare a variable:
MenuItem LastBrightnessMenuItem =null;
When we are considering a group of menuitems, there is a probability of using a single event handler. In this case we can use this logic:
private void BrightnessMenuClick(object sender, RoutedEventArgs e)
{
if (LastBrightnessMenuItem != null)
{
LastBrightnessMenuItem.IsChecked = false;
}
MenuItem m = sender as MenuItem;
LastBrightnessMenuItem = m;
//Handle the rest of the logic here
}
I find that I get mutually exclusive menu items when binding MenuItem.IsChecked to a variable.
But it has one quirk: If you click the selected menu item, it gets invalid, shown by the usual red rectangle. I solved it by adding a handler for MenuItem.Click that prevents unselecting by just setting IsChecked back to true.
The code... I'm binding to an enum type, so I use an enum converter that returns true if the bound property is equal to the supplied parameter. Here is the XAML:
<MenuItem Header="Black"
IsCheckable="True"
IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Black}"
Click="MenuItem_OnClickDisallowUnselect"/>
<MenuItem Header="Red"
IsCheckable="True"
IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Red}"
Click="MenuItem_OnClickDisallowUnselect"/>
And here is the code behind:
private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
if (menuItem == null) return;
if (! menuItem.IsChecked)
{
menuItem.IsChecked = true;
}
}
Several years after i see this post with the keywords i wrote... i thought there was an easy solution, in wpf... Perhaps it's me, but i think it's a bit special to have a such massive arsenal for a so little thing as accepted solution. I don't even talk about the solution with 6likes i didn't understood where to click to have this options.
So perhaps it's really no elegant at all... But here a simple solution. What it do is simple.. a loop to all elements contained by the parent, to put it at false. The most of time people split this part from the others parts, of course it's only correct in this case.
private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{
MenuItem itemChecked = (MenuItem)sender;
MenuItem itemParent = (MenuItem)itemChecked.Parent;
foreach (MenuItem item in itemParent.Items)
{
if (item == itemChecked)continue;
item.IsChecked = false;
}
}
that's all and easy, xaml is a classic code with absolutaly nothing particular
<MenuItem Header="test">
<MenuItem Header="1" Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
<MenuItem Header="2" Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
</MenuItem>
Of course you could have a need of the click method, it's not a problem, you can make a method that accept an object sender and each of your click method will use this method. It's old, it's ugly but for the while it works.
And i have some problems to imagine so much code line for a so little thing, it's probably me that have a problem with xaml, but it seems incredible to have to do this to obtains to just have only one menuitem selected.
A small addition to the #Patrick answer.
As #MK10 mentioned, this solution allows user to deselect all items in a group. But the changes he suggested doesn't work for me now. Maybe, the WPF model was changed since that time, but now Checked event doesn't fired when an item is unchecked.
To avoid it, I would suggest to process the Unchecked event for MenuItem.
I changed these procedures:
private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is MenuItem menuItem))
return;
var newGroupName = e.NewValue.ToString();
var oldGroupName = e.OldValue.ToString();
if (string.IsNullOrEmpty(newGroupName))
{
RemoveCheckboxFromGrouping(menuItem);
}
else
{
if (newGroupName != oldGroupName)
{
if (!string.IsNullOrEmpty(oldGroupName))
{
RemoveCheckboxFromGrouping(menuItem);
}
ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
menuItem.Checked += MenuItemChecked;
menuItem.Unchecked += MenuItemUnchecked; // <-- ADDED
}
}
}
private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
{
ElementToGroupNames.Remove(checkBox);
checkBox.Checked -= MenuItemChecked;
checkBox.Unchecked -= MenuItemUnchecked; // <-- ADDED
}
and added the next handler:
private static void MenuItemUnchecked(object sender, RoutedEventArgs e)
{
if (!(e.OriginalSource is MenuItem menuItem))
return;
var isAnyItemChecked = ElementToGroupNames.Any(item => item.Value == GetGroupName(menuItem) && item.Key.IsChecked);
if (!isAnyItemChecked)
menuItem.IsChecked = true;
}
Now the checked item remains checked when user clicks it second time.
Here is yet another way – not easy by any stretch but it is MVVM compatible, bindable and highly unit testable. If you have the freedom to add a Converter to your project and don’t mind a little garbage in the form of a new list of items every time the context menu opens, this works really well. It meets the original question of how to provide a mutually exclusive set of checked items in a context menu.
I think if you want to extract all of this into a user control you could make it into a reusable library component to reuse across your application.
Components used are Type3.Xaml with a simple grid, one text block and the context menu. Right-click anywhere in the grid to make the menu appear.
A value converter named AllValuesEqualToBooleanConverter is used to compare each menu item’s value to the current value of the group and show the checkmark next to the menu item that is currently selected.
A simple class that represent your menu choices is used for illustration. The sample container uses Tuple with String and Integer properties that make is fairly easy to have a tightly coupled human readable snippet of text paired with a machine-friendly value. You can use strings alone or String and an Enum to keep track of the Value for making decisions over what is current.
Type3VM.cs is the ViewModel that is assigned to the DataContext for Type3.Xaml. However you contrive to assign your data context in your existing application framework, use the same mechanism here. The application framework in use relies on INotifyPropertyChanged to communicate changed values to WPF and its binding goo. If you have dependency properties you may need to tweak the code a little bit.
The drawback to this implementation, aside from the converter and its length, is that a garbage list is created every time the context menu is opened. For single user applications this is probably ok but you should be aware of it.
The application uses an implementation of RelayCommand that is readily available from the Haacked website or any other ICommand-compatible helper class available in whatever framework you are using.
public class Type3VM : INotifyPropertyChanged
{
private List<MenuData> menuData = new List<MenuData>(new[]
{
new MenuData("Zero", 0),
new MenuData("One", 1),
new MenuData("Two", 2),
new MenuData("Three", 3),
});
public IEnumerable<MenuData> MenuData { get { return menuData.ToList(); } }
private int selected;
public int Selected
{
get { return selected; }
set { selected = value; OnPropertyChanged(); }
}
private ICommand contextMenuClickedCommand;
public ICommand ContextMenuClickedCommand { get { return contextMenuClickedCommand; } }
private void ContextMenuClickedAction(object clicked)
{
var data = clicked as MenuData;
Selected = data.Item2;
OnPropertyChanged("MenuData");
}
public Type3VM()
{
contextMenuClickedCommand = new RelayCommand(ContextMenuClickedAction);
}
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class MenuData : Tuple<String, int>
{
public MenuData(String DisplayValue, int value) : base(DisplayValue, value) { }
}
<UserControl x:Class="SampleApp.Views.Type3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Views="clr-namespace:SampleApp.Views"
xmlns:Converters="clr-namespace:SampleApp.Converters"
xmlns:ViewModels="clr-namespace:SampleApp.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance ViewModels:Type3VM}"
>
<UserControl.Resources>
<Converters:AllValuesEqualToBooleanConverter x:Key="IsCheckedVisibilityConverter" EqualValue="True" NotEqualValue="False" />
</UserControl.Resources>
<Grid>
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuData, Mode=OneWay}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem" >
<Setter Property="Header" Value="{Binding Item1}" />
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked">
<Setter.Value>
<MultiBinding Converter="{StaticResource IsCheckedVisibilityConverter}" Mode="OneWay">
<Binding Path="DataContext.Selected" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}" />
<Binding Path="Item2" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding Path=DataContext.ContextMenuClickedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}}" />
<Setter Property="CommandParameter" Value="{Binding .}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.RowDefinitions><RowDefinition Height="*" /></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="30" Text="Right Click For Menu" />
</Grid>
</UserControl>
public class AreAllValuesEqualConverter<T> : IMultiValueConverter
{
public T EqualValue { get; set; }
public T NotEqualValue { get; set; }
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
T returnValue;
if (values.Length < 2)
{
returnValue = EqualValue;
}
// Need to use .Equals() instead of == so that string comparison works, but must check for null first.
else if (values[0] == null)
{
returnValue = (values.All(v => v == null)) ? EqualValue : NotEqualValue;
}
else
{
returnValue = (values.All(v => values[0].Equals(v))) ? EqualValue : NotEqualValue;
}
return returnValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[ValueConversion(typeof(object), typeof(Boolean))]
public class AllValuesEqualToBooleanConverter : AreAllValuesEqualConverter<Boolean>
{ }
You can hook both check and uncheck event for the MenuItem and inside the event you can call a common method like below:
private void MenuItem_Unchecked(object sender, RoutedEventArgs e)
{
this.UpdateCheckeditem(sender as MenuItem);
}
private void MenuItem_Checked(object sender, RoutedEventArgs e)
{
this.UpdateCheckeditem(sender as MenuItem);
}
private void UpdateCheckedstatus(MenuItem item)
{
MenuItem itemChecked = (MenuItem)sender;
MenuItem itemParent = (MenuItem)itemChecked.Parent;
foreach (MenuItem item in itemParent.Items)
{
if (item != itemChecked && item.IsChecked)
{
item.IsChecked = false;
break;
}
}
}
I think this will give you the expected behavior.
Simply create a Template for MenuItem which will contain a RadioButton with a GroupName set to some value.
You can also change the template for the RadioButtons to look like the MenuItem's default check glyph (which can be easily extracted with Expression Blend).
That's it!
You could do something like this:
<Menu>
<MenuItem Header="File">
<ListBox BorderThickness="0" Background="Transparent">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<MenuItem IsCheckable="True" IsChecked="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Header="{Binding Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.Items>
<ListBoxItem Content="Test" />
<ListBoxItem Content="Test2" />
</ListBox.Items>
</ListBox>
</MenuItem>
</Menu>
It has some weird side effect visually (you'll see when you use it), but it works nonetheless
Here's another approach that uses RoutedUICommands, a public enum property, and DataTriggers. This is a pretty verbose solution. I unfortunately don't see any way of making the Style.Triggers smaller, because I don't know how to just say that the Binding Value is the only thing different? (BTW, for MVVMers this is a terrible example. I put everything in the MainWindow class just to keep things simple.)
MainWindow.xaml:
<Window x:Class="MutuallyExclusiveMenuItems.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:MutuallyExclusiveMenuItems"
Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
<CommandBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}"
CanExecute="CanExecute"
Executed="MenuItem1Execute" />
<CommandBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}"
CanExecute="CanExecute"
Executed="MenuItem2Execute" />
<CommandBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}"
CanExecute="CanExecute"
Executed="MenuItem3Execute" />
</Window.CommandBindings>
<Window.InputBindings>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}" Gesture="Ctrl+1"/>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}" Gesture="Ctrl+2"/>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}" Gesture="Ctrl+3"/>
</Window.InputBindings>
<DockPanel>
<DockPanel DockPanel.Dock="Top">
<Menu>
<MenuItem Header="_Root">
<MenuItem Command="{x:Static view:MainWindow.MenuItem1Cmd}"
InputGestureText="Ctrl+1">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem1}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{x:Static view:MainWindow.MenuItem2Cmd}"
InputGestureText="Ctrl+2">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem2}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{x:Static view:MainWindow.MenuItem3Cmd}"
InputGestureText="Ctrl+3">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem3}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
</MenuItem>
</Menu>
</DockPanel>
</DockPanel>
</Window>
MainWindow.xaml.cs:
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
namespace MutuallyExclusiveMenuItems
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
#region Enum Property
public enum CurrentItemEnum { EnumItem1, EnumItem2, EnumItem3 };
private CurrentItemEnum _currentMenuItem;
public CurrentItemEnum CurrentMenuItem
{
get { return _currentMenuItem; }
set
{
_currentMenuItem = value;
OnPropertyChanged("CurrentMenuItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion Enum Property
#region Commands
public static RoutedUICommand MenuItem1Cmd =
new RoutedUICommand("Item_1", "Item1cmd", typeof(MainWindow));
public void MenuItem1Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem1;
}
public static RoutedUICommand MenuItem2Cmd =
new RoutedUICommand("Item_2", "Item2cmd", typeof(MainWindow));
public void MenuItem2Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem2;
}
public static RoutedUICommand MenuItem3Cmd =
new RoutedUICommand("Item_3", "Item3cmd", typeof(MainWindow));
public void MenuItem3Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem3;
}
public void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
#endregion Commands
}
}
Here is a custom control that i've created for this purpose.
It handles correctly checking, unchecking, clicks events and group name changes.
If you want you can override the style of the menu item and change the checkmark to a radiomark, but is not necessary:
public class RadioMenuItem : MenuItem
{
private bool abortCheckChange = false;
[DefaultValue("")]
public string GroupName
{
get => (string)GetValue(GroupNameProperty);
set => SetValue(GroupNameProperty, value);
}
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.Register(nameof(GroupName), typeof(string), typeof(RadioMenuItem),
new PropertyMetadata("", (d, e) => ((RadioMenuItem)d).OnGroupNameChanged((string)e.OldValue, (string)e.NewValue)));
static RadioMenuItem()
{
IsCheckedProperty.OverrideMetadata(typeof(RadioMenuItem),
new FrameworkPropertyMetadata(null, (d, o) => ((RadioMenuItem)d).abortCheckChange ? d.GetValue(IsCheckedProperty) : o));
}
protected override DependencyObject GetContainerForItemOverride()
{
return new RadioMenuItem();
}
protected override void OnClick()
{
//This will handle correctly the click, but prevents the unchecking.
//So the menu item acts that is correctly clicked (e.g. the menu disappears
//but the user can only check, not uncheck the item.
if (IsCheckable && IsChecked) abortCheckChange = true;
base.OnClick();
abortCheckChange = false;
}
protected override void OnChecked(RoutedEventArgs e)
{
base.OnChecked(e);
//If the menu item is checked, other items of the same group will be unchecked.
if (IsChecked) UncheckOtherGroupItems();
}
protected virtual void OnGroupNameChanged(string oldGroupName, string newGroupName)
{
//If the menu item enters on another group and is checked, other items will be unchecked.
if (IsChecked) UncheckOtherGroupItems();
}
private void UncheckOtherGroupItems()
{
if (IsCheckable)
{
IEnumerable<RadioMenuItem> radioItems = Parent is ItemsControl parent ? parent.Items.OfType<RadioMenuItem>()
.Where((item) => item.IsCheckable && (item.DataContext == parent.DataContext || item.DataContext != DataContext)) : null;
if (radioItems != null)
{
foreach (RadioMenuItem item in radioItems)
{
if (item != this && item.GroupName == GroupName)
{
//This will uncheck all other items on the same group.
item.IsChecked = false;
}
}
}
}
}
}
Example:
<Grid Background="Red" HorizontalAlignment="Left" Height="125" Margin="139,120,0,0" VerticalAlignment="Top" Width="120">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem IsCheckable="True" Header="Normal check 1"/>
<MenuItem IsCheckable="True" Header="Normal check 2"/>
<Separator/>
<local:RadioMenuItem IsCheckable="True" Header="Radio check 1" GroupName="Group1"/>
<local:RadioMenuItem IsCheckable="True" Header="Radio check 2" GroupName="Group1"/>
<local:RadioMenuItem IsCheckable="True" Header="Radio check 3" GroupName="Group1"/>
<Separator/>
<local:RadioMenuItem IsCheckable="True" Header="Radio check 4" GroupName="Group2"/>
<local:RadioMenuItem IsCheckable="True" Header="Radio check 5" GroupName="Group2"/>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
Related
A Combobox binds to a list of custom combobox items where each Item contains a checkbox together with an Image. The user can click either the image or the checkbox to select the item.
Each item contains a relative image path, the Selection-State. The viewmodel generates the list CustomCheckboxItems which the view then binds to.
Now i want to show the Displaynames of all selected items in the Combobox as ... selected items ... together. How can i achieve that? I tried to attach a contentpresenter to the combobox without success since i do not know exactly where to attach it to. Also writing control templates did not do the trick for me.
In the end the combobox should look something like this (Link goes to cdn.syncfusion.com). The ViewModel contains already a comma separated string containing the selected items. How do i have to change the combobox to behave like this?
View:
<ComboBox ItemsSource="{Binding Path=ViewModel.CustomCheckBoxItems, Mode=OneTime}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:CustomCheckBoxItem}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ImagePath}">
<Image.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding SelectItem, Mode=OneWay}" />
</Image.InputBindings>
</Image>
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center"IsChecked="Binding Selected, Mode=TwoWay}" >
<TextBlock Text="{Binding DisplayName}" IsEnabled="False" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The CustomCheckBoxItem Implementation
//INotifyPropertryChanged implementation and other things excluded for brevity
public class CustomCheckBoxItem : INotifyPropertyChanged
{
public CheckboxItem(ItemType item, string imagePath)
{
Item = item;
try{ImagePath = "/" + currentAssemblyName + ";component/Images/" + imagePath;}catch(Exception){}
}
private bool selected;
public bool Selected
{
get => selected;
set
{
selected = value;
NotifyPropertyChanged();
}
}
public ICommand SelectItem => new RelayCommand(() =>
{
if (isInit)
{
Selected = !selected;
}
},false);
public string ImagePath { get; }
public string DisplayName => Item.GetDisplayName();
}
The solution is to disconnect the ContentPresenter, that hosts the selected item in the selection box, from the ComboBox.
The ComboBox sets the selection box value internally. There is no chance to modify this behavior. Since the selection box also serves as input field for the edit or search mode, this logic is quite complex. From this perspective, the design choice to make the complete related logic of the ComboBox internal makes much sense.
The first option is to simply override the default template of the ComboBox and remove the bindings on the ContentPresenter.ContentTemplate, ContentPresenter.ContentTemplateSelector and ContentPresenter.ContentStringFormat properties.
Then bind the ContentPresenter.Content property to the data source that holds the multi selection display names. The disadvantage is that the complete multi-select logic is outside the ComboBox (in the worst case, it even bleeds into the view model). As the multi-select behavior is part of the control, it should be implemented inside.
The second solution is to extend ComboBox to make the handling of the control more convenient and reusable. You probably don't want your view model to be responsible to participate in the multi-select logic e.g. by tracking selection states in the context of multi-select (which is not of interest from the view model's point of view) and by exposing the display values to represent the multi-select state. You view model should not care about display values or multi-select.
The third option is to implement your custom ComboBox and let it extend MultiSelector. If you require all the ComboBox features, then this solutions can take a while to implement and test properly. If you only need a basic multi-select dropdown control, then the task is quite simple and the cleanest solution.
Other options you may find, like hacking the edit mode to set the ComboBox.Text property, are not recommended as they interfere with the internal behavior: the logic around the IsEditable and IsReadOnly is quite complex. So better don't mess around with to prevent unexpected behavior.
Also, you lose many features like search and edit. If you don't care about those feature, then the best and cleanest solution is the third introduced option to implement a Custom control that extends MultiSelector.
Implementation
The following example implements the second solution. The MultiSelectComboBox extends ComboBox. It internally grabs the ContentPresenter, disconnects it from the ComboBox (by overriding its content and removing the internal templating) and tracks the selection state. The advantage of this solution is that you don't have to "hack" the edit mode of the ComboBox. Therefore, the edit mode feature remains untouched. This solution simply changes the displayed values in the default toggle box. Even the default single.select behavior remains intact.
The multi-select state is realized by overriding the ComboBoxItem template to replace the ContentPresenter with a ToggleButton or alternatively, create a ComboBoxToggleItem (like in the example below) that extends ComboBoxItem to make everything reusable and MVVM ready.
Additionally we need to introduce a custom IsItemSelected property by implementing it as an attached property. This is necessary because the ComboBoxItem.IsSelected property is controlled by the Selector (the superclass of ComboBox). And by making it attached, we can avoid a tight coupling between the MultiSelectComboBox logic and the ComboBoxToggleItem Everything still works with the default ComboBoxItem or any other item container. The Selector is also responsible to ensure that only a single item is selected. But we need multiple items to be selected simultaneously.
This way we can easily track selected items and expose them via a public SelectedItems property.
You can use the ItemContainerStyle to bind the data model's selection property (if existing) to this attached MultiSelectComboBox.IsItemSelected property.
By implementing a custom ComboBoxItem like the ComboBoxToggleItem and hard-coding the CheckBox into its ControlTemplate you are no longer forced to track the visual state in your view model. This offers a clean separation. The visibility of the CheckBox in this example is able to be toggled by handling the ComboBoxToggleItem.IsCheckBoxEnabled property.
Because the ComboBoxToggleItem is basically a ToggleButton, you can select the item without clicking the CheckBox. The CheckBox is now an optional feature that only serves to provide another visual feedback.
If you don't define the ItemTemplate, you can control the displayed value using the common DisplayMemberPath (ComboBox default behavior). The MultiSelectComboBox will pick the value from the designated member and concatenate it with the other selected values.
If you want to display different values for the drop-down panel and the selected content box, use the MultiSelectComboBox.SelectionBoxDisplayMemberPath property to specify the item's source property name (in the same manner like the DisplayMemberPath).
If you don't set neither DisplayMemberPath nor the SelectionBoxDisplayMemberPath, the MultiSelectComboBox will call object.ToString on the data item. This way you can even generate a computed value by overriding ToString on the model. This gives you three options to control the selection box display value, while the MultiSelectComboBox concatenates and displays them.
This way the complete logic that handles the displayed values was moved from view model to the view, where it belongs:
MultiSelectComboBox.cs
public class MultiSelectComboBox : ComboBox
{
public static void SetIsItemSelected
(UIElement attachedElement, bool value)
=> attachedElement.SetValue(IsItemSelectedProperty, value);
public static bool GetIsItemSelected(UIElement attachedElement)
=> (bool)attachedElement.GetValue(IsItemSelectedProperty);
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached(
"IsItemSelected",
typeof(bool),
typeof(MultiSelectComboBox),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsItemSelectedChanged));
public string SelectionBoxDisplayMemberPath
{
get => (string)GetValue(SelectionBoxDisplayMemberPathProperty);
set => SetValue(SelectionBoxDisplayMemberPathProperty, value);
}
public static readonly DependencyProperty SelectionBoxDisplayMemberPathProperty = DependencyProperty.Register(
"SelectionBoxDisplayMemberPath",
typeof(string),
typeof(MultiSelectComboBox),
new PropertyMetadata(default));
public IList<object> SelectedItems
{
get => (IList<object>)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList<object>),
typeof(MultiSelectComboBox),
new PropertyMetadata(default));
private static Dictionary<DependencyObject, ItemsControl> ItemContainerOwnerTable { get; }
private ContentPresenter PART_ContentSite { get; set; }
private Dictionary<UIElement, string> SelectionBoxContentValues { get; }
static MultiSelectComboBox() => MultiSelectComboBox.ItemContainerOwnerTable = new Dictionary<DependencyObject, ItemsControl>();
public MultiSelectComboBox()
{
this.SelectionBoxContentValues = new Dictionary<UIElement, string>();
this.SelectedItems = new List<object>();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindVisualChildElement(this, out ContentPresenter contentPresenter, false))
{
contentPresenter.ContentTemplate = null;
contentPresenter.ContentStringFormat = null;
contentPresenter.ContentTemplateSelector = null;
this.PART_ContentSite = contentPresenter;
}
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
this.SelectedItems.Clear();
MultiSelectComboBox.ItemContainerOwnerTable.Clear();
Dispatcher.InvokeAsync(InitializeSelectionBox, DispatcherPriority.Background);
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems)
{
var itemContainer = this.ItemContainerGenerator.ContainerFromItem(item);
MultiSelectComboBox.ItemContainerOwnerTable.Remove(itemContainer);
this.SelectedItems.Remove(item);
}
break;
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
this.SelectionBoxContentValues.Clear();
IEnumerable<(object Item, ComboBoxItem? ItemContainer)>? selectedItemInfos = this.ItemContainerGenerator.Items
.Select(item => (Item: item, ItemContainer: this.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem))
.Where(selectedItemInfo => GetIsItemSelected(selectedItemInfo.ItemContainer));
foreach (var selectedItemInfo in selectedItemInfos)
{
string memberPath = this.SelectionBoxDisplayMemberPath
?? this.DisplayMemberPath
?? selectedItemInfo.Item.ToString();
string itemDisplayValue = selectedItemInfo.Item.GetType().GetProperty(memberPath).GetValue(selectedItemInfo.Item)?.ToString()
?? selectedItemInfo.Item.ToString();
this.SelectionBoxContentValues.Add(selectedItemInfo.ItemContainer, itemDisplayValue);
MultiSelectComboBox.ItemContainerOwnerTable.TryAdd(selectedItemInfo.ItemContainer, this);
this.SelectedItems.Add(selectedItemInfo.Item);
}
UpdateSelectionBox();
}
protected override bool IsItemItsOwnContainerOverride(object item) => item is ComboBoxToggleItem;
protected override DependencyObject GetContainerForItemOverride() => new ComboBoxToggleItem();
private static void OnIsItemSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var comboBoxItem = d as ComboBoxItem;
if (MultiSelectComboBox.ItemContainerOwnerTable.TryGetValue(comboBoxItem, out ItemsControl owner))
{
var comboBoxItemOwner = owner as MultiSelectComboBox;
bool isUnselected = !GetIsItemSelected(comboBoxItem);
if (isUnselected)
{
comboBoxItemOwner.SelectionBoxContentValues.Remove(comboBoxItem);
comboBoxOwner.SelectedItems.Remove(comboBoxItem);
UpdateSelectionBox()
}
}
}
private static void UpdateSelectionBox()
{
string selectionBoxContent = string.Join(", ", this.SelectionBoxContentValues.Values);
if (this.IsEditable)
{
this.Text = selectionBoxContent;
}
else
{
this.PART_ContentSite.Content = selectionBoxContent;
}
}
private void OnItemUnselected(object sender, SelectionChangedEventArgs e)
{
foreach (var removedItem in e.RemovedItems)
{
this.SelectedItems.Remove(removedItem);
}
}
private void InitializeSelectionBox()
{
EnsureItemsLoaded();
UpdateSelectionBox();
}
private void EnsureItemsLoaded()
{
IsDropDownOpen = true;
IsDropDownOpen = false;
}
private static bool TryFindVisualChildElement<TChild>(DependencyObject parent,
out TChild resultElement,
bool isTraversingPopup = true)
where TChild : FrameworkElement
{
resultElement = null;
if (isTraversingPopup
&& parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild frameworkElement)
{
resultElement = frameworkElement;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement, isTraversingPopup))
{
return true;
}
}
return false;
}
}
ComboBoxToggleItem.cs
public class ComboBoxToggleItem : ComboBoxItem
{
public bool IsCheckBoxEnabled
{
get => (bool)GetValue(IsCheckBoxEnabledProperty);
set => SetValue(IsCheckBoxEnabledProperty, value);
}
public static readonly DependencyProperty IsCheckBoxEnabledProperty = DependencyProperty.Register(
"IsCheckBoxEnabled",
typeof(bool),
typeof(ComboBoxToggleItem),
new PropertyMetadata(default));
static ComboBoxToggleItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ComboBoxToggleItem), new FrameworkPropertyMetadata(typeof(ComboBoxToggleItem)));
}
// Add text search selection support
protected override void OnSelected(RoutedEventArgs e)
{
base.OnSelected(e);
MultiSelectComboBox.SetIsItemSelected(this, true);
}
}
Generic.xaml
<Style TargetType="local:ComboBoxToggleItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComboBoxToggleItem">
<ToggleButton x:Name="ToggleButton"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(local:MultiSelectComboBox.IsItemSelected)}">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding ElementName=ToggleButton, Path=IsChecked}"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsCheckBoxEnabled, Converter={StaticResource BooleanToVisibilityConverter}}" />
<ContentPresenter />
</StackPanel>
</ToggleButton>
<ControlTemplate.Triggers>
<Trigger SourceName="ToggleButton"
Property="IsChecked"
Value="True">
<Setter Property="IsSelected"
Value="True" />
</Trigger>
<Trigger SourceName="ToggleButton"
Property="IsChecked"
Value="False">
<Setter Property="IsSelected"
Value="False" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Usage example
DataItem.cs
class DataItem : INotifyPropertyChanged
{
string TextData { get; }
int Id { get; }
bool IsActive { get; }
}
MainViewModel
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<DataItem> DataItems { get; }
}
MainWIndow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<local:MultiSelectComboBox ItemsSource="{Binding DataItems}"
DisplayMemberPath="TextData"
SelectionBoxDisplayMemberPath="Id">
<local:MultiSelectComboBox.ItemContainerStyle>
<Style TargetType="local:ComboBoxToggleItem">
<Setter Property="local:MultiSelectComboBox.IsItemSelected"
Value="{Binding IsActive}" />
<Setter Property="IsCheckBoxEnabled"
Value="True" />
</Style>
</local:MultiSelectComboBox.ItemContainerStyle>
<local:MultiSelectComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataItem}">
<TextBlock Text="{Binding TextData}" />
</DataTemplate>
</local:MultiSelectComboBox.ItemTemplate>
</local:MultiSelectComboBox>
</Window>
If I am understanding correctly:
You just want all the checked items to be moved to top of combobox
You want comma separated list of selected items shown in closed combobox.
If that is correct, the solution is much simpler than you are thinking.
All that is needed is this:
/// <summary>
/// Sort the items in the list by selected
/// </summary>
private void cmb_DropDownOpened ( object sender, EventArgs e )
=> cmb.ItemsSource = CustomCheckBoxItems
.OrderByDescending ( c => c.Selected )
.ThenBy ( c => c.DisplayName );
/// <summary>
/// Display comma separated list of selected items
/// </summary>
private void cmb_DropDownClosed ( object sender, EventArgs e )
{
cmb.IsEditable = true;
cmb.IsReadOnly = true;
cmb.Text = string.Join ( ",", CustomCheckBoxItems.Where ( c => c.Selected )
.OrderBy ( c => c.DisplayName )
.Select ( c => c.DisplayName )
.ToList () );
}
The key to the solution is just to reorder the list every time the combo is opened you present a re-sorted list.
Every time it is closed, you gather selected and show.
Complete working example:
Given this XAML:
<ComboBox Name="cmb" DropDownOpened="cmb_DropDownOpened">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center" IsChecked="{Binding Selected, Mode=TwoWay}" >
<TextBlock Text="{Binding DisplayName}" IsEnabled="False" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
and this class (removed notify for this sample, since not needed)
public class CustomCheckBoxItem
{
public string Item { get; set; }
private const string currentAssemblyName = "samplename";
public CustomCheckBoxItem ( string item, string imagePath )
{
Item = item;
try
{ ImagePath = "/" + currentAssemblyName + ";component/Images/" + imagePath; }
catch ( Exception ) { }
}
public bool Selected { get; set; }
public string ImagePath { get; }
public string DisplayName => Item;
}
Then to test I just used this:
public ObservableCollection<CustomCheckBoxItem> CustomCheckBoxItems { get; set; }
= new ObservableCollection<CustomCheckBoxItem> ()
{
new ( "Item 1", "image1.png" ),
new ( "Item 2", "image2.png" ),
new ( "Item 3", "image3.png" ),
new ( "Item 4", "image4.png" ),
new ( "Item 5", "image5.png" ),
new ( "Item 6", "image6.png" ),
new ( "Item 7", "image7.png" ),
};
public MainWindow ()
{
InitializeComponent ();
cmb.ItemsSource = CustomCheckBoxItems;
}
/// <summary>
/// Sort the items in the list by selected
/// </summary>
private void cmb_DropDownOpened ( object sender, EventArgs e )
=> cmb.ItemsSource = CustomCheckBoxItems
.OrderByDescending ( c => c.Selected )
.ThenBy ( c => c.DisplayName );
/// <summary>
/// Display comma separated list of selected items
/// </summary>
private void cmb_DropDownClosed ( object sender, EventArgs e )
{
cmb.IsEditable = true;
cmb.IsReadOnly = true;
cmb.Text = string.Join ( ",", CustomCheckBoxItems.Where ( c => c.Selected )
.OrderBy ( c => c.DisplayName )
.Select ( c => c.DisplayName )
.ToList () );
}
Inspired by the post linked in the comments, I wondered how far I'd get using the base WPF building blocks, but without messing with the ComboBox' template itself. The answer from Heena uses a WrapPanel to show a (filtered) SelectedItems collection that updates on the fly.
<ListBox Background="Transparent" IsHitTestVisible="False" BorderBrush="Transparent" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled" BorderThickness="0" ItemsSource="{Binding ElementName=lst,Path=SelectedItems}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ContentData}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I figured a DataTemplateSelector, to differentiate between the SelectedItemTemplate and the DropdownItemsTemplate, in combination with a CollectionView, that has a filter applied, should get us there.
CheckBoxItem.cs
public class CheckboxItem : ViewModelBase
{
public CheckboxItem(string name, string imagePath)
{
DisplayName = name;
ImagePath = imagePath;
SelectItem = new DummyCommand();
}
public string ImagePath { get; }
public string DisplayName { get; }
public ICommand SelectItem { get; }
private bool selected;
public bool Selected
{
get => selected;
set
{
selected = value;
OnPropertyChanged();
ItemSelectionChanged?.Invoke(this, new EventArgs());
}
}
public event EventHandler ItemSelectionChanged;
}
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Items = new ObservableCollection<CheckboxItem>
{
new CheckboxItem("smile", "/Images/Smiley.png"),
new CheckboxItem("wink", "/Images/Wink.png"),
new CheckboxItem("shocked", "/Images/Shocked.png"),
new CheckboxItem("teeth", "/Images/Teeth.png"),
};
SelectedItems = (CollectionView)new CollectionViewSource { Source = Items }.View;
SelectedItems.Filter = o => o is CheckboxItem item && item.Selected;
foreach(var item in Items)
{
item.ItemSelectionChanged += (_, _) => SelectedItems.Refresh();
}
// Set a (dummy) SelectedItem for the TemplateSelector to kick in OnLoad.
SelectedItem = Items.First();
}
public ObservableCollection<CheckboxItem> Items { get; }
public CollectionView SelectedItems { get; }
public CheckboxItem SelectedItem { get; }
}
ComboBoxTemplateSelector.cs
public class ComboBoxTemplateSelector : DataTemplateSelector
{
public DataTemplate SelectedItemTemplate { get; set; }
public DataTemplate DropdownItemsTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or a ComboBoxItem (or null).
// This will determine which template to use.
while (itemToCheck is not null and not ComboBox and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown.
return itemToCheck is ComboBoxItem ? DropdownItemsTemplate : SelectedItemTemplate;
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
Title="MultiSelectComboBox" Height="350" Width="400">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:ComboBoxTemplateSelector x:Key="ComboBoxTemplateSelector">
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding DataContext.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}"
BorderThickness="0" IsHitTestVisible="False" BorderBrush="Transparent" Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
<local:ComboBoxTemplateSelector.DropdownItemsTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=ImagePath}" Height="50">
<Image.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding SelectItem, Mode=OneWay}" />
</Image.InputBindings>
</Image>
<CheckBox VerticalAlignment="Center" VerticalContentAlignment="Center" Margin="20 0"
IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
</CheckBox>
</StackPanel>
</DataTemplate>
</local:ComboBoxTemplateSelector.DropdownItemsTemplate>
</local:ComboBoxTemplateSelector>
</Window.Resources>
<Grid>
<ComboBox ItemsSource="{Binding Path=Items}"
SelectedItem="{Binding SelectedItem, Mode=OneTime}"
ItemTemplateSelector="{StaticResource ComboBoxTemplateSelector}"
Height="30" Width="200" Margin="0 30 0 0"
VerticalAlignment="Top" HorizontalAlignment="Center">
</ComboBox>
</Grid>
</Window>
The ViewModel contains already a comma separated string containing the selected items. How do i have to change the combobox to behave like this?
You could swap
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding DataContext.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}"
BorderThickness="0" IsHitTestVisible="False" BorderBrush="Transparent" Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CheckboxItem}">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
With
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.CommaSeperatedString, RelativeSource={RelativeSource AncestorType={x:Type ComboBox}}}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
You could also do without the CommaSeperatedString property and combine the CollectionView with Greg M's string.Join approach inside a converter:
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.SelectedItems,
RelativeSource={RelativeSource AncestorType={x:Type ComboBox}},
Converter={StaticResource ItemsToCommaSeparatedStringConverter}}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
Resources:
WrapPanel idea:
CheckComboBox : How to prevent a combobox from closing after a selection is clicked?
Using different templates:
Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?
Filtering ItemsSource:
custom search for combobox
Smileys:
https://pngimg.com/images/miscellaneous/smiley
I am trying to bind an ObservableCollection of objects to a TreeView. The object class is shown below
public partial class Layer_Properties : Window
{
//fields
private string _routeName;
private List<Stop> _Stops_List = new List<Stop>();
public string routeName // property
{
get { return _routeName; } // get method
set { _routeName = value; } // set method
}
public List<Stop> Stops_List // property
{
get { return _Stops_List; } // get method
set { _Stops_List = value; } // set method
}
public List<PolylineBarrier> polylineBarriers// property
{
get { return _polylineBarriers; } // get method
set { _polylineBarriers= value; } // set method
}
public Layer_Properties(RouteTask asolveRouteTask, MapViewModel aMapViewModel)
{
InitializeComponent();
solveRouteTask = asolveRouteTask;
_mapViewModel = aMapViewModel;
this.Loaded += async (o, e) =>
{
await Task.Run(() => getnetworkDatasetProprties(solveRouteTask));
routeGUID = Convert.ToString(Guid.NewGuid());
dateTime_label.IsEnabled = false;
dateTime_ComboBox.IsEnabled = false;
this.dateTime_ComboBox.Value = DateTime.UtcNow;
Use_Time_Windows_chkbox.IsEnabled = false;
Use_Time_Windows_chkbox.IsEnabled = false;
Use_Time_Windows_chkbox.IsEnabled = false;
PreserveFirstStop_chkbox.IsChecked = true;
PreserveLastStop_chkbox.IsChecked = true;
PreserveFirstStop_chkbox.IsEnabled = false;
PreserveLastStop_chkbox.IsEnabled = false;
Layer_Properties item = null;
if (_mapViewModel.LayersPool.Count <= 1)
{
item = _mapViewModel.LayersPool[_mapViewModel.LayersPool.Count - 1];
}
else
{
item = _mapViewModel.LayersPool[_mapViewModel.LayersPool.Count - 2];
}
if (item != null)
{
item.routeName = IndexedFilename("Route", item.routeName);
}
};
}
Part of the mapviewmodel is shown below:
public class MapViewModel : INotifyPropertyChanged
{
public MapViewModel()
{
}
//test 09062020
public ObservableCollection<Layer_Properties> LayersPool
{
get { return layersPool; }
set
{
layersPool = value;
NotifiyPropertyChanged("LayersPool");
}
}
private ObservableCollection<Layer_Properties> layersPool= new
ObservableCollection<Layer_Properties>();
void NotifiyPropertyChanged(string property)
{
if (LayersPoolChanged != null)
LayersPoolChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler LayersPoolChanged;
//endtest
}
Part of the xaml is shown below:
<Window x:Class="GIS_App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
xmlns:local="clr-namespace:GIS_App"
mc:Ignorable="d"
Title="MainWindow" Height="732" Width="1399" Closing="Window_Closing">
<Window.Resources>
<local:MapViewModel x:Key="MapViewModel"/>
<ImageBrush x:Key="NetworkAnalystWindow" ImageSource="/icons/NetworkAnalystWindow.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="AddNetworkElement" ImageSource="/icons/AddNetworkElement_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="solveRoute" ImageSource="/icons/solve_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="solvePremiumRoute" ImageSource="/icons/solvePremium_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="RouteDirections" ImageSource="/icons/directions_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="AddTrafficLayer" ImageSource="/icons/AddTrafficLayer.png" Stretch="UniformToFill"/>
<ContextMenu x:Key="cmButton">
<MenuItem Name ="Draw_Sketch" Header="Draw Sketch" Click="btn_Click"/>
<MenuItem Name ="Save_Sketch" Header="Save Sketch" Click="btn_Click"/>
<MenuItem Name ="Delete_Sketch" Header="Delete Sketch" Click="btn_Click" IsEnabled="True"/>
<MenuItem Name ="Remove_Selected_Vertex" Header="Remove Selected Vertex" Click="btn_Click" IsEnabled="True"/>
<MenuItem Name ="Cancel" Header="Cancel" Click="btn_Click" IsEnabled="True"/>
<Separator />
<MenuItem Header="Address Geocoding" Click="btn_Click" IsEnabled="True"/>
</ContextMenu>
<ContextMenu x:Key="treeViewMenu">
<MenuItem Name ="Delete_TreeViewElement" Header="Delete" Click="btn_Click"/>
<MenuItem Name ="Delete_All_TreeViewElements" Header="Delete All" Click="btn_Click"/>
<MenuItem Name ="Open_Attribute_Table" Header="Open_Attribute_Table" Click="btn_Click" IsEnabled="True"/>
</ContextMenu>
</Window.Resources>
<Grid HorizontalAlignment="Left" Height="Auto" Margin="5,25,0,20" VerticalAlignment="Stretch" Width="155">
<!-- hieracical binding -->
<TextBlock Text="Hierarchical root binding}" Foreground="Red" Margin="10,20,0,0"/>
<TreeView ItemsSource="{Binding LayersPool}" Margin="10" Height="200">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding LayersPool}" DataType="{x:Type local:Layer_Properties}">
<TreeViewItem Header="{Binding routeName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
i ve made numerous attempts to make it work but i cannot find a solution. What i want to achieve is create a treeview as follows:
Route_1
Stops
Stop1
Stop2
Stop3
Polyline Barriers
Barrier 1
Route_2
Stops
Stop1
Stop2
Polyline Barriers
Barrier 1
Barrier2
Edit I suspect there might be an issue with binding since LayersPool observablecollection is a mapviewmodel collection. Any help will be welcome.
I think you need to understand how TreeView binding works.
You can have a look here: wpf treeview binding and also here: https://www.wpf-tutorial.com/treeview-control/treeview-data-binding-multiple-templates/
Generally speaking, if each TreeViewItem ViewModel needs to do something specific (for example once the user click on it, then you need to create your own ViewModel specific class for each TreeViewItem.
You might create an AbstractViewModel as a base class for every TreeViewItem ViewModel that contains basically 2 properties.
Children binded to ItemsSource in the HierarchicalDataTemplate that contains an ObservableCollection of current treeviewitem's leaves
Name binded to your Text property of the TextBlock you placed in your DataTemplate to show the treeview item text.
I'm working towards making click and drag-able spline curves while learning WPF. I've been able to successfully work with pure Line segments, but making the jump to a polyline is proving difficult. I have a class for interpolating the spline curves that I used to use in WinForms, so I'm using a few input clicks from the mouse, and those will be the thumbs to click and drag. The interpolated points have a high enough resolution that a WPF Polyline should be fine for display. To clarify, I need the higher resolution output, so using a WPF Beizer is not going to work.
I have the outline pretty well setup- but the particular issue I'm having, is that dragging the thumbs does not either a) the two way binding is not setup correctly, or b) the ObservableCollection is not generating notifications. I realize that the ObservableCollection only notifies when items are added/removed/cleared, etc, and not that the individual indices are able to produce notifications. I have spent the last few hours searching- found some promising ideas, but haven't been able to wire them up correctly. There was some code posted to try inherit from ObservableCollection and override the OnPropertyChanged method in the ObservableCollection, but that's a protected virtual method. While others used a method call into the OC to attach PropertyChanged event handlers to each object, but I'm unsure where to inject that logic. So I am a little stuck.
MainWindow.xaml:
There is an ItemsControl hosted in a mainCanvas. ItemsControl is bound to a property on the ViewModel
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem x:Name="menuAddNewPolyline" Header="Add Polyline" Click="MenuItem_Click" />
</Menu>
<Canvas x:Name="mainCanvas" Grid.Row="1">
<ItemsControl x:Name="polylinesItemsControl"
ItemsSource="{Binding polylines}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</Grid>
MainWindow.Xaml.cs:
Pretty simple- initializes a new view model, and it's set as the DataContext. There is a menu with a Add Polyline item, which in turn, initializes a new PolylineControl, and generates three random points (using Thread.Sleep, otherwise they were the same, between the calls) within the ActualHeight and ActualWidth of the window. The new PolylineControl is added to the ViewModel in an ObservableCollection This is a stand in until I get to accepting mouse input.
public partial class MainWindow : Window
{
private ViewModel viewModel;
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
DataContext = viewModel;
}
private Point GetRandomPoint()
{
Random r = new Random();
return new Point(r.Next(0, (int)this.ActualWidth), r.Next(0, (int)this.ActualHeight));
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
var newPolyline = new PolylineControl.Polyline();
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
viewModel.polylines.Add(newPolyline);
}
}
ViewModel.cs:
Absolutely noting fancy here
public class ViewModel
{
public ObservableCollection<PolylineControl.Polyline> polylines { get; set; }
public ViewModel()
{
polylines = new ObservableCollection<PolylineControl.Polyline>();
}
}
**The PolylineControl:
Polyline.cs:**
Contains DP's for an ObservableCollection of points for the polyline. Eventually this will also contain the interpolated points as well as the input points, but a single collection of points will do for the demo. I did try to use the INotifyPropertyChanged interface to no avail.
public class Polyline : Control
{
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PolylinePoints", typeof(ObservableCollection<Point>), typeof(Polyline),
new FrameworkPropertyMetadata(new ObservableCollection<Point>()));
public ObservableCollection<Point> PolylinePoints
{
get { return (ObservableCollection<Point>)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
static Polyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Polyline), new FrameworkPropertyMetadata(typeof(Polyline)));
}
}
Generic.xaml
Contains a canvas with a databound Polyline, and an ItemsControl with a DataTemplate for the ThumbPoint control.
<local:PointCollectionConverter x:Key="PointsConverter"/>
<Style TargetType="{x:Type local:Polyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Polyline}">
<Canvas Background="Transparent">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
Points="{Binding Path=PolylinePoints,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource PointsConverter}}"
>
</Polyline>
<ItemsControl x:Name="thumbPoints"
ItemsSource="{Binding PolylinePoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint Point="{Binding Path=., Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
PointsCollectionConverter.cs:
Contains a IValueConverter to turn the ObservableCollection into a PointsCollection.
public class PointCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() == typeof(ObservableCollection<Point>) && targetType == typeof(PointCollection))
{
var pointCollection = new PointCollection();
foreach (var point in value as ObservableCollection<Point>)
{
pointCollection.Add(point);
}
return pointCollection;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
And finally, the ThumbPointControl:
ThumbPoint.cs:
Contains a single DP for the center of the point, along with the DragDelta functionality.
public class ThumbPoint : Thumb
{
public static readonly DependencyProperty PointProperty =
DependencyProperty.Register("Point", typeof(Point), typeof(ThumbPoint),
new FrameworkPropertyMetadata(new Point()));
public Point Point
{
get { return (Point)GetValue(PointProperty); }
set { SetValue(PointProperty, value); }
}
static ThumbPoint()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ThumbPoint), new FrameworkPropertyMetadata(typeof(ThumbPoint)));
}
public ThumbPoint()
{
this.DragDelta += new DragDeltaEventHandler(this.OnDragDelta);
}
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
this.Point = new Point(this.Point.X + e.HorizontalChange, this.Point.Y + e.VerticalChange);
}
}
Generic.xaml:
Contains the style, and an Ellipse bound which is databound.
<Style TargetType="{x:Type local:ThumbPoint}">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
<Setter Property="Margin" Value="-4"/>
<Setter Property="Background" Value="Gray" />
<Setter Property="Canvas.Left" Value="{Binding Path=Point.X, RelativeSource={RelativeSource Self}}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Point.Y, RelativeSource={RelativeSource Self}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ThumbPoint}">
<Ellipse x:Name="PART_Ellipse"
Fill="{TemplateBinding Background}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Window after the Add Polyline menu item is pressed
The code works to add the polyline with three random points.
Thumbs moved away from poly line
However, once you move the thumbs, the polyline does not update along with it.
I have a working example of just a single line segment (added to the view model as many times as you click the add segment button) so it seems the logic should all be correct, but something broke down with the introduction of the ObservableCollection to host the multiple points required for a polyline.
Any help is appreciated
Following on from Clemens suggestions, I was able to make it work.
I renamed the Polyline.cs control to eliminate confusion with the standard WPF Polyline Shape class to DynamicPolyline. The class now implements INotifyPropertyChanged, and has DP for the PolylinePoints and a seperate ObservableCollection for a NotifyingPoint class which also implements INotifyPropertyChanged. When DynamicPolyline is initialized, it hooks the CollectionChanged event on the ObserableCollection. The event handler method then either adds an event handler to each item in the collection, or removes it based on the action. The event handler for each item simply calls SetPolyline, which in turn cycles through the InputPoints adding them to a new PointCollection, and then sets the Points property on the PART_Polyline (which a reference to is created in the OnApplyTemplate method).
It turns out the Points property on a Polyline does not listen to the INotifyPropertyChanged interface, so data binding in the Xaml was not possible. Probably will end up using a PathGeometery in the future, but for now, this works.
To address Marks non MVVM concerns.. It's a demo app, sorry I had some code to test things in the code behind. The point is to be able to reuse these controls, and group them with others for various use cases, so it makes more sense for them to be on their own vs repeating the code.
DynmicPolyline.cs:
public class DynamicPolyline : Control, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PoilylinePoints", typeof(PointCollection), typeof(DynamicPolyline),
new PropertyMetadata(new PointCollection()));
public PointCollection PolylinePoints
{
get { return (PointCollection)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
private ObservableCollection<NotifyingPoint> _inputPoints;
public ObservableCollection<NotifyingPoint> InputPoints
{
get { return _inputPoints; }
set
{
_inputPoints = value;
OnPropertyChanged();
}
}
private void SetPolyline()
{
if (polyLine != null && InputPoints.Count >= 2)
{
var newCollection = new PointCollection();
foreach (var point in InputPoints)
{
newCollection.Add(new Point(point.X, point.Y));
}
polyLine.Points = newCollection;
}
}
private void InputPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged += InputPoints_PropertyChange;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged -= InputPoints_PropertyChange;
}
}
}
private void InputPoints_PropertyChange(object sender, PropertyChangedEventArgs e)
{
SetPolyline();
}
public DynamicPolyline()
{
InputPoints = new ObservableCollection<NotifyingPoint>();
InputPoints.CollectionChanged += InputPoints_CollectionChanged;
SetPolyline();
}
static DynamicPolyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DynamicPolyline), new FrameworkPropertyMetadata(typeof(DynamicPolyline)));
}
private Polyline polyLine;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
polyLine = this.Template.FindName("PART_Polyline", this) as Polyline;
}
NotifyingPoint.cs
Simple class that raises property changed events when X or Y is updated from the databound ThumbPoint.
public class NotifyingPoint : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public event EventHandler ValueChanged;
private double _x = 0.0;
public double X
{
get { return _x; }
set
{
_x = value;
OnPropertyChanged();
ValueChanged?.Invoke(this, null);
}
}
private double _y = 0.0;
public double Y
{
get { return _y; }
set
{
_y = value;
OnPropertyChanged();
}
}
public NotifyingPoint()
{
}
public NotifyingPoint(double x, double y)
{
X = x;
Y = y;
}
public Point ToPoint()
{
return new Point(_x, _y);
}
}
And finally, for completeness, here is the Generic.xaml for the control. Only change in here was the bindings for X and Y of the NotifyingPoint.
<Style TargetType="{x:Type local:DynamicPolyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DynamicPolyline}">
<Canvas x:Name="PART_Canvas">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
/>
<ItemsControl x:Name="PART_ThumbPointItemsControl"
ItemsSource="{Binding Path=InputPoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint X="{Binding Path=X, Mode=TwoWay}" Y="{Binding Path=Y, Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I dropped my Spline class in to the SetPolyline method, and got the result I was after:
Two working click and drag able spline curves
Let's say I'm writing an ItemsControl control template, and need to refer to the ItemsPanel instance for some reason. Since not in the same namescope, there's not really a way to bind to it.
I'm using a custom items panel -- a carousel-type panel that slides from one item to the next -- and would like a button in the control template to maneuver left or right:
<Style TargetType="custom:AnItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<custom:SlideContentPanel />
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="custom:AnItemsControl">
<Grid>
<ItemsPresenter />
<Button Content="Next" HorizontalAlignment="Right" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
How can the Button in this example refer to methods like "GoRight()" and properties "CanGoRight" on the items panel "SlideContentPanel"?
You can't refer to the items panel instance directly. But you can define a dependency property on the ItemsControl, and set it in code-behind when the control loads. Then the control template can refer to that property via a TemplateBinding.
So in the example, "AnItemsControl" should define a dependency property of type "SlideContentPanel":
public SlideControl()
{
DefaultStyleKey = typeof(SlideControl);
Loaded += SlideControl_Loaded;
}
public SlideContentPanel TypedPanel
{
get { return (SlideContentPanel)GetValue(TypedPanelProperty); }
set { SetValue(TypedPanelProperty, value); }
}
public static readonly DependencyProperty SlidePanelProperty =
DependencyProperty.Register("TypedPanel", typeof(SlideContentPanel), typeof(AnItemsControl), new PropertyMetadata(null));
Now, when the control loads, locate the panel using VisualTreeHelper, and set the dependency property:
public AnItemsControl()
{
DefaultStyleKey = typeof(AnItemsControl);
Loaded += AnItemsControl_Loaded;
}
private void AnItemsControl_Loaded(object sender, RoutedEventArgs e)
{
TypedPanel = FindItemsPanel<SlideContentPanel>(this);
}
private T FindItemsPanel<T>(FrameworkElement visual)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
FrameworkElement child = VisualTreeHelper.GetChild(visual, i) as FrameworkElement;
if (child != null)
{
if (child is T && VisualTreeHelper.GetParent(child) is ItemsPresenter)
{
object temp = child;
return (T)temp;
}
T panel = FindItemsPanel<T>(child);
if (panel != null)
{
object temp = panel;
return (T)temp;
}
}
}
return default(T);
}
And now the button in "AnItemsControl"'s control template can refer to the panel by using the "TypedPanel" property in the templated parent:
<Button Content="Next" HorizontalAlignment="Right"
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=TypedPanel.CanGoRight,
Converter={StaticResource BoolToVisibilityConverter}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap">
<ei:CallMethodAction
TargetObject="{Binding RelativeSource={RelativeSource TemplatedParent},Path=TypedPanel}"
MethodName="GoRight" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
I've got a MenuItem whos ItemsSource is databound to a simple list of strings, its showing correctly, but I'm struggling to see how I can handle click events for them!
Here's a simple app that demonstrates it:
<Window x:Class="WPFDataBoundMenu.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Menu>
<MenuItem Header="File" Click="MenuItem_Click" />
<MenuItem Header="My Items" ItemsSource="{Binding Path=MyMenuItems}" />
</Menu>
</Grid>
using System.Collections.Generic;
using System.Windows;
namespace WPFDataBoundMenu
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public List<string> MyMenuItems { get; set;}
public Window1()
{
InitializeComponent();
MyMenuItems = new List<string> { "Item 1", "Item 2", "Item 3" };
DataContext = this;
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("how do i handle the other clicks?!");
}
}
}
Many thanks!
Chris.
<MenuItem Header="My Items" ItemsSource="{Binding Path=MyMenuItems}" Click="DataBoundMenuItem_Click" />
Code behind..
private void DataBoundMenuItem_Click(object sender, RoutedEventArgs e)
{
MenuItem obMenuItem = e.OriginalSource as MenuItem;
MessageBox.Show( String.Format("{0} just said Hi!", obMenuItem.Header));
}
Events will bubble up to the common handler. You can then use the Header text or better the DataContext property to switch and act as needed
You could have each menu item execute the same command, thus handling the execution centrally. If you need to distinguish menu items beyond the actual object instance, you can bind the command parameter too:
<MenuItem Header="My Items" ItemsSource="{Binding Path=MyMenuItems}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{x:Static local:MyCommands.MyCommand}"/>
<Setter Property="CommandParameter" Value="{Binding SomeProperty}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
SomeProperty is assumed to be a property on each item in your MyMenuItems collection. Your command execution handler would therefore receive the value of SomeProperty for the particular menu item that is clicked.
IMHO more general event handler with ability to get item from itemssource
private void DataBoundMenuItem_Click(object sender, RoutedEventArgs e)
{
// get menu item with ItemsSource bound
var myItemsMenuItems = sender as MenuItem;
// get submenu clicked item constructed from MyMenuItems collection
var myItemsMenuSubItem = e.OriginalSource as MenuItem;
// get underlying MyMenuItems collection item
var o = myItemsMenuItems
.ItemContainerGenerator
.ItemFromContainer(myItemsMenuSubItem);
// convert to MyMenuItems type ... in our case string
var itemObj = o as (string);
// TODO some processing
}
Hope it'l help smbd!
If you want a simpler way to access the menu item content:
<MenuItem Header="My Items" ItemsSource="{Binding Path=MyMenuItems}" Click="MenuItem_Click">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="CommandParameter" Value="{Binding}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
Cod Behind:
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
var item = ((MenuItem)e.OriginalSource).CommandParameter;
}