I have a ListView, and I would like to change behaviour, so SelectionChanged event would fire on MouseUp, instead of MouseDown.
The reason why I want to change behaviour, is because when I want to drag objects (on MouseMove), the selection is changed when I click (MouseDown) on the objects to move.
I found that piece of code, but this is working only for simple selection, and I would like to allow selecting several items.
public static class SelectorBehavior
{
#region bool ShouldSelectItemOnMouseUp
public static readonly DependencyProperty ShouldSelectItemOnMouseUpProperty =
DependencyProperty.RegisterAttached(
"ShouldSelectItemOnMouseUp", typeof(bool), typeof(SelectorBehavior),
new PropertyMetadata(default(bool), HandleShouldSelectItemOnMouseUpChange));
public static void SetShouldSelectItemOnMouseUp(DependencyObject element, bool value)
{
element.SetValue(ShouldSelectItemOnMouseUpProperty, value);
}
public static bool GetShouldSelectItemOnMouseUp(DependencyObject element)
{
return (bool)element.GetValue(ShouldSelectItemOnMouseUpProperty);
}
private static void HandleShouldSelectItemOnMouseUpChange(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Selector selector)
{
selector.PreviewMouseDown -= HandleSelectPreviewMouseDown;
selector.MouseUp -= HandleSelectMouseUp;
if (Equals(e.NewValue, true))
{
selector.PreviewMouseDown += HandleSelectPreviewMouseDown;
selector.MouseUp += HandleSelectMouseUp;
}
}
}
private static void HandleSelectMouseUp(object sender, MouseButtonEventArgs e)
{
var selector = (Selector)sender;
if (e.ChangedButton == MouseButton.Left && e.OriginalSource is Visual source)
{
var container = selector.ContainerFromElement(source);
if (container != null)
{
var index = selector.ItemContainerGenerator.IndexFromContainer(container);
if (index >= 0)
{
selector.SelectedIndex = index;
}
}
}
}
private static void HandleSelectPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
e.Handled = e.ChangedButton == MouseButton.Left;
}
#endregion
}
Here is my Xaml :
<ListView x:Name="ListViewContract" SelectedItem="{Binding SelectedContract}" ItemsSource="{Binding ListContractsView}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" MouseDoubleClick="ListViewContract_DoubleClick" GridViewColumnHeader.Click ="GridViewHeaderClicked" SelectionChanged="ListViewContract_SelectionChanged" Visibility="{Binding Grid1Visible, Converter={StaticResource BoolToVisConverter}}"
MouseMove="ListViewContract_MouseMove" local:SelectorBehavior.ShouldSelectItemOnMouseUp="True">
<ListView.View>
<GridView AllowsColumnReorder="true" x:Name="GridViewContract">
<GridViewColumn DisplayMemberBinding="{Binding ID}" Header="ID" Width="{Binding WidthIDs, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="{x:Static p:Resources.Name}" Width="{Binding WidthColumnContractName, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Code}" Header="{x:Static p:Resources.Number}" Width="{Binding WidthColumnContractCode, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Comm}" Header="{x:Static p:Resources.Commentary}" Width="{Binding WidthColumnContractComm, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Revision}" Header="{x:Static p:Resources.Revision}" Width="{Binding WidthColumnContractRev, Mode=TwoWay}"/>
<GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdv, Mode=TwoWay}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<Rectangle Name="AFF_Track" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgression}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
<Rectangle Name="AFF_Track2" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdvRep, Mode=TwoWay}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<Rectangle Name="AFF_TrackRep" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRep}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
<Rectangle Name="AFF_Track2Rep" Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRepWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
<Rectangle.ToolTip>
<ContentControl Template="{StaticResource ToolTipOperations}"/>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding Manager.CompleteName}" Header="{x:Static p:Resources.ProjectManager}" Width="{Binding WidthColumnContractManager, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Client.Name}" Header="{x:Static p:Resources.Customer}" Width="{Binding WidthColumnContractCustomer, Mode=TwoWay}"/>
<GridViewColumn DisplayMemberBinding="{Binding Source}" Header="{x:Static p:Resources.Source}" Width="{Binding WidthColumnContractSource, Mode=TwoWay}"/>
</GridView>
</ListView.View>
</ListView>
You can modify your behavior to handle controls that extend MultiSelector (like the DataGrid) and ListBox individually. ListBox (and therefore ListView too) is not a MultiSelector but supports multi selection when setting the ListBox.SelectionMode property to something different than SelectionMode.Single (which is the default).
For every other simple Selector you would have to track the selected items manually. You would also have to intercept the Selector.Unselected event to prevent the Selector from unselecting the selected items when in a multi select mode - Selector only supports single item selection.
The following example shows how you can track selected items in case the attached Selector is not a ListBox or a MultiSelector. For this reason the behavior exposes a public readonly SelectedItems dependency property.
The example also shows how to observe the pressed keyboard keys in order to filter multi select user actions. This example filters Shift or Ctrl keys as gesture to trigger the multi select behavior: random multi select while CTRL is pressed and range select while Shift key is pressed. Otherwise the Selector will behave as usual (single select on release of the left mouse button).
SelectorService.cs
public class SelectorService : DependencyObject
{
#region IsSelectItemOnMouseUpEnabled attached property
public static readonly DependencyProperty IsSelectItemOnMouseUpEnabledProperty = DependencyProperty.RegisterAttached(
"IsSelectItemOnMouseUpEnabled",
typeof(bool),
typeof(SelectorService),
new PropertyMetadata(default(bool), OnIsSelectItemOnMouseUpEnabledChanged));
public static void SetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement, bool value) => attachedElement.SetValue(IsSelectItemOnMouseUpEnabledProperty, value);
public static bool GetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement) => (bool)attachedElement.GetValue(IsSelectItemOnMouseUpEnabledProperty);
#endregion IsSelectItemOnMouseUpEnabled attached property
#region SelectedItems attached property
public static IList GetSelectedItems(DependencyObject attachedElement) => (IList)attachedElement.GetValue(SelectedItemsProperty);
public static void SetSelectedItems(DependencyObject attachedElement, IList value) => attachedElement.SetValue(SelectedItemsPropertyKey, value);
private static readonly DependencyPropertyKey SelectedItemsPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
"SelectedItems",
typeof(IList),
typeof(SelectorService),
new PropertyMetadata(default));
public static readonly DependencyProperty SelectedItemsProperty = SelectedItemsPropertyKey.DependencyProperty;
#endregion SelectedItems attached property
#region SelectionMode attached property (private)
private static SelectionMode GetOriginalSelectionModeBackup(DependencyObject attachedElement) => (SelectionMode)attachedElement.GetValue(OriginalSelectionModeBackupProperty);
private static void SetOriginalSelectionModeBackup(DependencyObject attachedElement, SelectionMode value) => attachedElement.SetValue(OriginalSelectionModeBackupProperty, value);
private static readonly DependencyProperty OriginalSelectionModeBackupProperty = DependencyProperty.RegisterAttached(
"OriginalSelectionModeBackup",
typeof(SelectionMode),
typeof(SelectorService),
new PropertyMetadata(default));
#endregion SelectionMode attached property
private static bool IsRandomMultiSelectEngaged
=> Keyboard.Modifiers is ModifierKeys.Control;
private static bool IsRangeMultiSelectEngaged
=> Keyboard.Modifiers is ModifierKeys.Shift;
private static Dictionary<Selector, bool> IsMultiSelectAppliedMap { get; } = new Dictionary<Selector, bool>();
private static int SelectedRangeStartIndex { get; set; } = -1;
private static int SelectedRangeEndIndex { get; set; } = -1;
private static void OnIsSelectItemOnMouseUpEnabledChanged(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
{
if (attachedElement is not Selector selector)
{
return;
}
if ((bool)e.NewValue)
{
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);
if (selector.IsLoaded)
{
InitializeAttachedElement(selector);
}
else
{
selector.Loaded += OnSelectorLoaded;
}
}
else
{
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);
SetSelectedItems(selector, null);
IsMultiSelectAppliedMap.Remove(selector);
selector.Loaded -= OnSelectorLoaded;
if (selector is ListBox listBox)
{
listBox.SelectionMode = GetOriginalSelectionModeBackup(listBox);
}
}
}
private static void OnSelectorLoaded(object sender, RoutedEventArgs e)
{
var selector = sender as Selector;
selector.Loaded -= OnSelectorLoaded;
InitializeAttachedElement(selector);
}
private static void InitializeAttachedElement(Selector selector)
{
IList selectedItems = new List<object>();
if (selector is ListBox listBox)
{
ValidateListBoxSelectionMode(listBox);
selectedItems = listBox.SelectedItems;
}
else if (selector is MultiSelector multiSelector)
{
selectedItems = multiSelector.SelectedItems;
}
else if (selector.SelectedItem is not null)
{
selectedItems.Add(selector.SelectedItem);
}
SetSelectedItems(selector, selectedItems);
IsMultiSelectAppliedMap.Add(selector, false);
}
private static void OnUnselected(object? sender, RoutedEventArgs e)
{
var itemContainer = sender as DependencyObject;
Selector.SetIsSelected(itemContainer, true);
Selector.RemoveUnselectedHandler(itemContainer, OnUnselected);
}
private static void OnPreviewLeftMouseButtonUp(object sender, MouseButtonEventArgs e)
{
var selector = sender as Selector;
DependencyObject itemContainerToSelect = ItemsControl.ContainerFromElement(selector, e.OriginalSource as DependencyObject);
DependencyObject currentSelectedItemContainer = selector.ItemContainerGenerator.ContainerFromItem(selector.SelectedItem);
if (itemContainerToSelect is null)
{
return;
}
if (IsRandomMultiSelectEngaged)
{
MultiSelectItems(selector, itemContainerToSelect, currentSelectedItemContainer);
}
else if (IsRangeMultiSelectEngaged)
{
MultiSelectRangeOfItems(selector, itemContainerToSelect, currentSelectedItemContainer);
}
else
{
SingleSelectItem(selector, itemContainerToSelect, currentSelectedItemContainer);
}
IsMultiSelectAppliedMap[selector] = IsRandomMultiSelectEngaged || IsRangeMultiSelectEngaged;
}
private static void MultiSelectRangeOfItems(Selector selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
// In case there is not any preselected item. Otherwis SingleSlectItem() has already set the SelectedRangeStartIndex property.
if (SelectedRangeStartIndex == -1)
{
SelectedRangeStartIndex = clickedContainerIndex;
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(SelectedRangeStartIndex);
MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
return;
}
// Complete the range selection
else if (SelectedRangeEndIndex == -1)
{
bool isSelectionRangeFromTopToBotton = clickedContainerIndex > SelectedRangeStartIndex;
if (isSelectionRangeFromTopToBotton)
{
SelectedRangeEndIndex = clickedContainerIndex;
}
else
{
// Selection is from bottom to top, so we need to swap start and end index
// as they are used to initialize the for-loop.
SelectedRangeEndIndex = SelectedRangeStartIndex;
SelectedRangeStartIndex = clickedContainerIndex;
}
for (int itemIndex = SelectedRangeStartIndex; itemIndex <= SelectedRangeEndIndex; itemIndex++)
{
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(itemIndex);
bool isContainerUnselected = !Selector.GetIsSelected(itemContainer);
if (isContainerUnselected)
{
MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
}
}
// Only remember start index to append sequential ranges (more clicks while Shift key is pressed)
// and invalidate the end index.
SelectedRangeEndIndex = -1;
}
}
private static void MultiSelectItems(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
bool oldIsSelectedValue = Selector.GetIsSelected(itemContainerToSelect);
// Toggle the current state
bool newIsSelectedValue = oldIsSelectedValue ^= true;
if (selector is ListBox listBox)
{
// In case the mode was overriden externally, force it back
// but store the changed value to allow roll back when the behavior gets disabled.
ValidateListBoxSelectionMode(listBox);
}
// If the current Selector instance does not support native multi select
// we need to prevent the Selector from unselecting previous selected items.
// By setting unselected items back to selected we can enforce a visual multi select feedback.
if (selector is not MultiSelector and not ListBox)
{
if (newIsSelectedValue && currentSelectedItemContainer is not null)
{
Selector.AddUnselectedHandler(currentSelectedItemContainer, OnUnselected);
}
}
Selector.SetIsSelected(itemContainerToSelect, newIsSelectedValue);
(itemContainerToSelect as UIElement)?.Focus();
if (selector is not MultiSelector and not ListBox)
{
object item = selector.ItemContainerGenerator.ItemFromContainer(itemContainerToSelect);
IList selectedItems = GetSelectedItems(selector);
if (newIsSelectedValue)
{
selectedItems.Add(item);
}
else
{
selectedItems.Remove(item);
}
}
}
private static void SingleSelectItem(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
{
bool isPreviousSelectMultiSelect = IsMultiSelectAppliedMap[selector];
if (!isPreviousSelectMultiSelect)
{
// Unselect the currently selected
if (currentSelectedItemContainer is not null)
{
Selector.SetIsSelected(currentSelectedItemContainer, false);
}
}
// If the Selector has multiple selected items and an item was clicked without the modifier key pressed,
// then we need to switch back to single selection mode and only select the currently clicked item.
else
{
// Invalidate tracked multi select range
SelectedRangeStartIndex = -1;
SelectedRangeEndIndex = -1;
if (selector is ListBox listBox)
{
ValidateListBoxSelectionMode(listBox);
listBox.UnselectAll();
}
else if (selector is MultiSelector multiSelector)
{
multiSelector.UnselectAll();
}
else
{
IList selectedItems = GetSelectedItems(selector);
foreach (object item in selectedItems)
{
DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromItem(item);
Selector.SetIsSelected(itemContainer, false);
}
selectedItems.Clear();
}
}
// Execute single selection
Selector.SetIsSelected(itemContainerToSelect, true);
(itemContainerToSelect as UIElement)?.Focus();
if (selector is not MultiSelector and not ListBox)
{
IList selectedItems = GetSelectedItems(selector);
selectedItems.Clear();
selectedItems.Add(selector.SelectedItem);
}
// Track index in case the next click enabled select range (press Shift while click)
int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
SelectedRangeStartIndex = clickedContainerIndex;
return;
}
private static void ValidateListBoxSelectionMode(ListBox listBox)
{
if (listBox.SelectionMode is not SelectionMode.Extended)
{
// In case the mode was overriden externally, force it back
// but store the changed value to allow roll back when the behavior gets disabled.
SetOriginalSelectionModeBackup(listBox, listBox.SelectionMode);
listBox.SelectionMode = SelectionMode.Extended;
}
}
private static void OnPreviewLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
=> e.Handled = true;
}
In addition to #BionicCode 's code : the code doesn't take in charge DoubleClick. I could solve it adding that code : (BionicCode, if you could edit answer for future visitors, thanks).
-Add a static variable in the class :
public static DateTime lastClickDateTime = DateTime.Now;
Then at the very beginning of SingleSelectItem function :
TimeSpan interval = DateTime.Now - lastClickDateTime;
if(interval.TotalMilliseconds<500)
{
if(selector is ListBox listBox)
{
MouseButtonEventArgs doubleClickEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)DateTime.Now.Ticks, MouseButton.Left);
doubleClickEvent.RoutedEvent = Control.MouseDoubleClickEvent;
doubleClickEvent.Source = selector;
selector.RaiseEvent(doubleClickEvent);
return;
}
}
lastClickDateTime = DateTime.Now;
Thanks again for your help :)
Related
I am using WPF Datagrid and have overridden OnSelectionChange to support multiple selection from UI as well as from code.
Right now if i press down arrow key on any selected item, selection and focus both moves to next row. I want only focus to move and not selection. I want to select the focused item when space key or escape key is pressed not on up/down arrow key. How to get this behavior. Here is my code -
<Controls:DMDataGrid Grid.Row="2" x:Name="dgrObjects"
ItemsSource="{Binding ListOfObjects, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Visible" AutoGenerateColumns="False"
ScrollViewer.VerticalScrollBarVisibility="Visible" GridLinesVisibility="None"
CanUserAddRows="false" Background="White"
IsReadOnly="True" HeadersVisibility="Column"
DMItemsList="{Binding SelectedObjects, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended" SelectionUnit="FullRow" CanUserReorderColumns="True"
CanUserSortColumns="True" Focusable="True">
<Controls:DMDataGrid.Resources>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsObjectSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</Style>
</Controls:DMDataGrid.Resources>
Here is code for Selection change behavior-
public class DMDataGrid : DataGrid
{
public DMDataGrid()
{
SelectionChanged += DMDataGrid_SelectionChanged;
}
void DMDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems != null)
{
foreach (var item in e.AddedItems)
{
if (item.GetType() == typeof(myclass))
{
(myclass)) tempItem = ((myclass)))item;
if (tempItem.IsSelected)
{
// if bound data item still is selected, fix this
tempItem.IsSelected = true;
}
}
}
}
if (e.RemovedItems != null)
{
foreach (var item in e.RemovedItems)
{
if (item.GetType() == typeof((myclass))))
{
(myclass)) tempItem = ((myclass)))item;
if (tempItem.IsSelected)
{
// if bound data item still is selected, fix this
tempItem.IsSelected = false;
}
}
}
}
e.Handled = true;
DMItemsList = SelectedItems;
}
public ICommand UnselectAllCommand { get { return new RelayCommand<object>((object o) => UnselectAll()); } }
public IList DMItemsList
{
get { return (IList)GetValue(DMItemsListProperty); }
set { SetValue(DMItemsListProperty, value); }
}
private static void OnSelectedItemsListChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
}
public static readonly DependencyProperty DMItemsListProperty = DependencyProperty.Register("DMItemsList", typeof(IList), typeof(DMDataGrid), new PropertyMetadata(OnSelectedItemsListChanged));
}
I'm creating a custom WPF control (CheckedListComboBox) that allows a user to select one or more options from a list. Essentially the user opens a dropdown-style control and checks off the items they would like to select. When the user checks or unchecks an option in the list, the main (non-popup) area of the control is updated to reflect the choices the user has made.
Here's an example of the control in action:
I am mostly satisfied with the state of the control, but there is one piece I would like to improve and I'm unsure of how to proceed.
The generation of the text that reflects the user's choice(s) currently relies on calling ToString on each of the selected items. This is fine in the example case above, as all of the objects I've passed to the control are strings. However, if I passed in a custom object that didn't override ToString, I would simply get the fully qualified class of the object (e.g. MyNamespace.MyObject).
What I would like to do is implement something akin to a WPF ComboBox's DisplayMemberPath property, where I can simply specify the property to display in the TextBox area of the control for each selected item.
I could just be lazy and go about this via Reflection, but I'm aware that this may not be the fastest (performance-wise) approach.
Here are the options I've considered so far:
Force all bound items to implement a specific interface - if all items passed to the control implemented an interface, such as ICheckableItem, I could then safely access the same property on each item to populate the TextBox. I'm shying away from this approach as I would like the control to be open about what sort of items it can accept.
Use an ItemsControl to display the text, instead of generating it in the code-behind - theoretically I could maintain a list of checked items privately within the control, bind that list to an ItemsControl within the control itself and then somehow bind to the property determined in DisplayMemberPath. I don't know if this is even possible, as I think it would have to do some sort of double-binding magic to work, and I don't think that is doable. There are also issues with getting the separator (in this case a comma) to appear if I follow this route.
The options I've listed above don't seem to work, and I can't think of any more possible approaches. Can anyone offer any other solutions?
Here's my code so far:
XAML
<UserControl
x:Class="MyNamespace.CheckedListComboBox"
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"
d:DesignHeight="30"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="TextBoxHolderGrid">
<TextBox
x:Name="PART_TextBox"
MaxWidth="{Binding ActualWidth, ElementName=TextBoxHolderGrid}"
Focusable="False"
IsReadOnly="True"
TextWrapping="NoWrap" />
</Grid>
<ToggleButton
x:Name="PART_ToggleButton"
Grid.Column="1"
Margin="-1,0,0,0"
HorizontalAlignment="Right"
ClickMode="Press"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
IsTabStop="False">
<Path
x:Name="CollapsedArrow"
Margin="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 4 4 L 8 0 Z">
<Path.Fill>
<SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
</Path.Fill>
</Path>
</ToggleButton>
<Popup
x:Name="PART_Popup"
Grid.ColumnSpan="2"
MinWidth="{Binding ActualWidth, ElementName=MainGrid}"
MaxHeight="{Binding MaxPopupHeight, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Margin="0,-1,0,0"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Placement="Bottom"
StaysOpen="False">
<Border BorderThickness="1">
<Border.BorderBrush>
<SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
</Border.BorderBrush>
<ScrollViewer x:Name="PART_DropDownScrollViewer" BorderThickness="1">
<ItemsControl
ItemTemplate="{Binding ItemTemplate, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
ItemsSource="{Binding ItemsSource, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
KeyboardNavigation.DirectionalNavigation="Contained">
<ItemsControl.Background>
<SolidColorBrush Color="{x:Static SystemColors.ControlLightLightColor}" />
</ItemsControl.Background>
</ItemsControl>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</UserControl>
Code behind
public partial class CheckedListComboBox : UserControl
{
private bool mouseIsOverPopup = false;
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(CheckedListComboBox), new PropertyMetadata(null));
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<CheckedListObject>), typeof(CheckedListComboBox), new PropertyMetadata(null, ItemsSourcePropertyChanged));
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(CheckedListComboBox), new PropertyMetadata(false, IsDropDownOpenChanged));
public static readonly DependencyProperty MaxPopupHeightProperty = DependencyProperty.Register("MaxPopupHeight", typeof(double), typeof(CheckedListComboBox), new PropertyMetadata((double)200));
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public IEnumerable<CheckedListObject> ItemsSource
{
get { return (IEnumerable<CheckedListObject>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public bool IsDropDownOpen
{
get { return (bool)GetValue(IsDropDownOpenProperty); }
set { SetValue(IsDropDownOpenProperty, value); }
}
public double MaxPopupHeight
{
get { return (double)GetValue(MaxPopupHeightProperty); }
set { SetValue(MaxPopupHeightProperty, value); }
}
public CheckedListComboBox()
{
InitializeComponent();
}
private static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CheckedListComboBox checkedListComboBox)
{
if (e.OldValue != null && e.OldValue is IEnumerable<CheckedListObject> oldItems)
{
foreach (var item in oldItems)
{
item.PropertyChanged -= checkedListComboBox.SubItemPropertyChanged;
}
}
if (e.OldValue != null && e.OldValue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= checkedListComboBox.ItemsSourceCollectionChanged;
}
if (e.NewValue != null && e.NewValue is IEnumerable<CheckedListObject> newItems)
{
foreach (var item in newItems)
{
item.PropertyChanged += checkedListComboBox.SubItemPropertyChanged;
}
}
if (e.NewValue != null && e.NewValue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += checkedListComboBox.ItemsSourceCollectionChanged;
}
checkedListComboBox.CalculateComboBoxText();
}
}
private void SubItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("IsChecked", StringComparison.OrdinalIgnoreCase))
{
CalculateComboBoxText();
}
}
private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// The bound ItemsSource collection has changed, so we want to unsubscribe any old items
// from the PropertyChanged event, and subscribe any new ones to this event.
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
if (oldItem is CheckedListObject item)
{
item.PropertyChanged -= SubItemPropertyChanged;
}
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
if (newItem is CheckedListObject item)
{
item.PropertyChanged += SubItemPropertyChanged;
}
}
}
// We also want to re-calculate the text in the ComboBox, in case any checked items
// have been added or removed.
CalculateComboBoxText();
}
private void CalculateComboBoxText()
{
var checkedItems = ItemsSource?.Where(item => item.IsChecked);
if (checkedItems?.Any() ?? false)
{
PART_TextBox.Text = string.Join(", ", checkedItems.Select(i => i.Item?.ToString()));
}
else
{
PART_TextBox.Text = "";
}
}
private static void IsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CheckedListComboBox box)
{
if (box.IsDropDownOpen)
{
Mouse.Capture(box, CaptureMode.SubTree);
}
else
{
if (Mouse.Captured?.Equals(box) ?? false)
{
Mouse.Capture(null);
}
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (IsDropDownOpen)
{
var textBoxPoint = e.GetPosition(PART_TextBox);
var popupPoint = e.GetPosition(PART_Popup.Child);
var mouseIsOverTextBox = !(textBoxPoint.X < 0 || textBoxPoint.X > PART_TextBox.ActualWidth || textBoxPoint.Y < 0 || textBoxPoint.Y > PART_TextBox.ActualHeight);
var mouseIsOverPopup = !(popupPoint.X < 0 || popupPoint.X > PART_Popup.Child.RenderSize.Width || popupPoint.Y < 0 || popupPoint.Y > PART_Popup.Child.RenderSize.Height);
if (mouseIsOverPopup && !mouseIsOverTextBox)
{
mouseIsOverPopup = true;
}
else
{
mouseIsOverPopup = false;
}
}
else
{
mouseIsOverPopup = false;
}
base.OnMouseMove(e);
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (IsDropDownOpen && !mouseIsOverPopup)
{
IsDropDownOpen = false;
}
base.OnMouseLeftButtonDown(e);
}
}
And here's the wrapper class for each of the items.
public class CheckedListObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private object item;
public CheckedListItem()
{ }
public CheckedListItem(object item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public object Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
}
And finally, here's how I'm using the control:
<p:CheckedListComboBox Grid.Row="2" ItemsSource="{Binding Items}">
<p:CheckedListComboBox.ItemTemplate>
<DataTemplate>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox Margin="0,0,6,0" IsChecked="{Binding IsChecked}" />
<TextBlock Grid.Column="1" Text="{Binding Item}" />
</Grid>
</DataTemplate>
</p:CheckedListComboBox.ItemTemplate>
</p:CheckedListComboBox>
I have a list view that binding items with a property in viewmodel.
<ListView Height="238"
HorizontalAlignment="Left"
Name="listView"
VerticalAlignment="Top"
Width="503"
ItemsSource="{Binding BusinessCollection}"
SelectionMode="Multiple">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListViewItem}}, Path=IsSelected}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding ID}" Header="ID" />
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Name" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
and in viewmodel.
ICollectionView _businessCollection
public ICollectionView BusinessCollection
{
get { return _businessCollection; }
set {
_businessCollection = value;
RaisePropertyOnChange("BusinessCollection");
}
}
How to get selected item of businesscollection in viewmodel?
1. One way to source binding:
You have to use SelectionChanged event. The easiest way is to write eventhandler in codebehind to "bind selecteditems" to viewmodel.
//ViewModel
public ICollectionView BusinessCollection {get; set;}
public List<YourBusinessItem> SelectedObject {get; set;}
//Codebehind
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (ViewModel) DataContext;
viewmodel.SelectedItems = listview.SelectedItems
.Cast<YourBusinessItem>()
.ToList();
}
This still aligns with MVVM design, because view and viewmodel resposibilities are kept separated. You dont have any logic in codebehind and viewmodel is clean and testable.
2. Two way binding:
if you also need to update view, when viewmodel changes, you have to attach to ViewModel's PropertyChanged event and to the selected items' CollectionChanged event. of course you can do it in codebehind, but in this case I would create something more reusable:
//ViewModel
public ObservableCollection<YourBusinessItem> SelectedObject {get; set;}
//in codebehind:
var binder = new SelectedItemsBinder(listview, viewmodel.SelectedItems);
binder.Bind();
or can create custom attached property, so you can use binding syntax in xaml:
<ListView local:ListViewExtensions.SelectedValues="{Binding SelectedItem}" .../>
public class SelectedItemsBinder
{
private ListView _listView;
private IList _collection;
public SelectedItemsBinder(ListView listView, IList collection)
{
_listView = listView;
_collection = collection;
_listView.SelectedItems.Clear();
foreach (var item in _collection)
{
_listView.SelectedItems.Add(item);
}
}
public void Bind()
{
_listView.SelectionChanged += ListView_SelectionChanged;
if (_collection is INotifyCollectionChanged)
{
var observable = (INotifyCollectionChanged) _collection;
observable.CollectionChanged += Collection_CollectionChanged;
}
}
public void UnBind()
{
if (_listView != null)
_listView.SelectionChanged -= ListView_SelectionChanged;
if (_collection != null && _collection is INotifyCollectionChanged)
{
var observable = (INotifyCollectionChanged) _collection;
observable.CollectionChanged -= Collection_CollectionChanged;
}
}
private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var item in e.NewItems ?? new object[0])
{
if (!_listView.SelectedItems.Contains(item))
_listView.SelectedItems.Add(item);
}
foreach (var item in e.OldItems ?? new object[0])
{
_listView.SelectedItems.Remove(item);
}
}
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.AddedItems ?? new object[0])
{
if (!_collection.Contains(item))
_collection.Add(item);
}
foreach (var item in e.RemovedItems ?? new object[0])
{
_collection.Remove(item);
}
}
}
Attached property implementation
public class ListViewExtensions
{
private static SelectedItemsBinder GetSelectedValueBinder(DependencyObject obj)
{
return (SelectedItemsBinder)obj.GetValue(SelectedValueBinderProperty);
}
private static void SetSelectedValueBinder(DependencyObject obj, SelectedItemsBinder items)
{
obj.SetValue(SelectedValueBinderProperty, items);
}
private static readonly DependencyProperty SelectedValueBinderProperty = DependencyProperty.RegisterAttached("SelectedValueBinder", typeof(SelectedItemsBinder), typeof(ListViewExtensions));
public static readonly DependencyProperty SelectedValuesProperty = DependencyProperty.RegisterAttached("SelectedValues", typeof(IList), typeof(ListViewExtensions),
new FrameworkPropertyMetadata(null, OnSelectedValuesChanged));
private static void OnSelectedValuesChanged(DependencyObject o, DependencyPropertyChangedEventArgs value)
{
var oldBinder = GetSelectedValueBinder(o);
if (oldBinder != null)
oldBinder.UnBind();
SetSelectedValueBinder(o, new SelectedItemsBinder((ListView)o, (IList)value.NewValue));
GetSelectedValueBinder(o).Bind();
}
public static void SetSelectedValues(Selector elementName, IEnumerable value)
{
elementName.SetValue(SelectedValuesProperty, value);
}
public static IEnumerable GetSelectedValues(Selector elementName)
{
return (IEnumerable)elementName.GetValue(SelectedValuesProperty);
}
}
Since the itemSource is BusinessCollection, you should be able to:
var selectedItems = BusinessCollection.Where(x => x.IsSelected);
Wrap it as a property on your VM.
Hope it helps!
I am using twoway binding in a checkbox data column in Telerik Grid view control. when I change the state of checkbox on UI, it is working fine triggering the property changed event. But I also want vice versa, on changing the property value on code behind, the checkbox state should also update on UI.
<Button x:Name="btn1" Grid.Row="0" Content="Refresh" Click="btn1_Click" Width="100" Margin="0,5"/>
<telerik:RadGridView Grid.Row="1" x:Name="gridView" ShowGroupPanel="False" IsFilteringAllowed="False" SelectionMode="Multiple">
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Width="70" Header="Color">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding ChangeValue, Mode=TwoWay}" Margin="3"/>
</StackPanel>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn Header="Data" DataMemberBinding="{Binding dataVal}" IsReadOnly="True"/>
</telerik:RadGridView.Columns>
</telerik:RadGridView>
</Grid>
The code behind is below
public partial class MainWindow : Window
{
List<Data> dataSource = new List<Data>();
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
{
data d = new Data();
d.DataVal= "Data " + (i + 1);
dataSource.Add(d);
}
this.gridView.SelectedItems.Clear();
PopulateGridView();
}
private void PopulateGridView()
{
foreach (Data d in dataSource)
{
d.DataVal= false;
}
this.gridView.ItemsSource = dataSource;
List<Data> selectedItems = new List<Data>();
selectedItems.Add(dataSource[0]);
this.gridView.Select(selectedItems);
}
private void btn1_Click(object sender, RoutedEventArgs e)
{
PopulateGridView();
}
}
public class Data: INotifyPropertyChanged
{
public Data()
{
DataVal = string.Empty;
}
public string DataVal { get; set; }
public bool ChangeValue
{
get { return changevalue; }
set
{
if (value != changevalue)
{
changevalue= value;
if (ApplyPropertyChanged!= null)
{
ApplyPropertyChanged(this, new PropertyChangedEventArgs("ChangeValue"));
}
}
}
}
private bool changevalue;
public event PropertyChangedEventHandler ApplyPropertyChanged;
}
On clicking Refresh button I want all checkbox unchecked but they are not updating when I am setting ChangeValue to false in PopulateGridView. Please suggest how can I achieve this.
You need to specify when the UI shoul be updated, in this case onpropertychanged
<CheckBox IsChecked="{Binding ChangeValue, Mode=TwoWay, UpdateSourceTrigger="PropertyChanged"}" Margin="3"/>
I am attempting to extend TabControl so that I can add and delete items, I have previously done this by adding a close command to my viewmodel that raises an event and a subscription in the parent viewmodel will remove the item from the collection.
I would like to make this approach more generic and am attempting to implement the ApplicationCommands.Delete command.
ExtendedTabControl.cs
public class ExtendedTabControl : TabControl
{
public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));
public bool CanUserDeleteTabs
{
get { return (bool)GetValue(CanUserDeleteTabsProperty); }
set { SetValue(CanUserDeleteTabsProperty, value); }
}
public static RoutedUICommand DeleteCommand
{
get { return ApplicationCommands.Delete; }
}
private IEditableCollectionView EditableItems
{
get { return (IEditableCollectionView)Items; }
}
private bool ItemIsSelected
{
get
{
if (this.SelectedItem != CollectionView.NewItemPlaceholder)
return true;
return false;
}
}
private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
{
((WorkspacesTabControl)sender).OnCanExecuteDelete(e);
}
private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// The Delete command needs to have CanExecute run.
CommandManager.InvalidateRequerySuggested();
}
private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
{
return ((WorkspacesTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
}
private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
{
((WorkspacesTabControl)sender).OnExecutedDelete(e);
}
static ExtendedTabControl()
{
Type ownerType = typeof(ExtendedTabControl);
DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
}
protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
{
// User is allowed to delete and there is a selection.
e.CanExecute = CanUserDeleteTabs && ItemIsSelected;
e.Handled = true;
}
#endregion
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
object currentItem = SelectedItem;
int indexToSelect = Items.IndexOf(currentItem) - 1;
if (currentItem != CollectionView.NewItemPlaceholder)
EditableItems.Remove(currentItem);
// This should focus the row and bring it into view.
SetCurrentValue(SelectedItemProperty, Items[indexToSelect]);
}
e.Handled = true;
}
private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
{
// Only when the base value is true do we need to validate
// that the user can actually add or delete rows.
if (baseValue)
{
if (!this.IsEnabled)
{
// Disabled TabControls cannot be modified.
return false;
}
else
{
if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
{
// The collection view does not allow the add or delete action.
return false;
}
}
}
return baseValue;
}
}
Generic.xaml
<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="CloseableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
<Style x:Key="{x:Type local:ExtendedTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" TargetType="{x:Type local:ExtendedTabControl}">
<Setter Property="ItemTemplate" Value="{StaticResource CloseableTabItemHeader}" />
</Style>
This nearly works, I can choose an item and remove it by either hitting my close button or using the delete key. However if I hit the close button of an item that isn't selected, it still removes the selected item. The reason for this behavior is obvious, but I'm not sure how to access the correct object for removal? I also need to assign the indexToSelect found in OnExecutedDelete in a better fashion though I'm comfortable I'll find a solution for this.
ExecutedRoutedEventArgs has property Parameter. Try to set DataContext of TabItem as CommandParameter:
<DataTemplate x:Key="CloseableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="{Binding .}" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
Then you can access DataContext in OnExecutedDelete:
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
object currentItem = e.Parameter ?? SelectedItem;
int indexToSelect = Items.IndexOf(currentItem) - 1;
...
}
e.Handled = true;
}
I've just done something very similar in http://dragablz.net
Two check-ins for managing add and remove of tabs:
https://github.com/ButchersBoy/Dragablz/commit/47bbc302f5ffeaa8c234269ab4ff11bc80f7fa10
https://github.com/ButchersBoy/Dragablz/commit/c1dce0435683db83f163a77ccb9a19b3218b3ca7