KeyBinding woes - wpf

I have a user control that as shown below, for a master detail sort of display. A typical MVVM architecture with a base view model that comes complete with a CloseCommand.
I am trying to scope a KeyBinding that will execute the close command on a TabItem, and just can't get it to work.
Interestingly, I can get it to work if I put the binding on the PersonDetailView (one of two possible USerControls that the TabControl might display, as shown below), but it should be on the TabControl or the Border that contains it.
Any suggestions?
Cheers,
Berryl
UserControl
<Grid>
<ListBox Style="{StaticResource ListBoxStyle}" />
<GridSplitter
HorizontalAlignment="Right" VerticalAlignment="Stretch" Grid.Column="1"
ResizeBehavior="PreviousAndNext" Width="5" Background="#FFBCBCBC" KeyboardNavigation.IsTabStop="False"
/>
<Border Grid.Column="2" Background="{StaticResource headerBrush}">
// ** THIS is the scope I want, but it doesn't work
<Border.InputBindings>
<KeyBinding Key="F4" Modifiers="Control" Command="{Binding CloseCommand}"/>
</Border.InputBindings>
<TabControl Style="{StaticResource TabControlStyle}" >
<TabControl.Resources>
<DataTemplate DataType="{x:Type personVm:PersonDetailVm}">
<local:PersonDetailView />
</DataTemplate>
<DataTemplate DataType="{x:Type orgVm:OrganizationDetailVm}">
<local:OrganizationDetailView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
</Border>
</Grid>
TabItem style
<Style x:Key="OrangeTabItemStyle" TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border AllowDrop="true" ToolTip="{Binding DisplayName}">
<Border Name="Border" Background="Transparent" BorderBrush="Transparent" BorderThickness="1,1,1,0" CornerRadius="2,2,0,0">
<DockPanel x:Name="TitlePanel" TextElement.Foreground="{StaticResource FileTabTextBrush}">
<ctrl:GlyphButton
// ** This works as expected
Command="{Binding CloseCommand}" CommandParameter="{Binding}"
>
</ctrl:GlyphButton>
</DockPanel>
</Border>
// ** Can't get it to work from here either **
<Border.InputBindings>
<KeyBinding Command="{Binding CloseCommand}" Key="F4" Modifiers="Control" />
</Border.InputBindings>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
UPDATE
I am at a loss to set the RoutedCommand in my style
<Style x:Key="OrangeTabItemStyle" TargetType="{x:Type TabItem}">
<Setter Property="beh:RoutedCommandWire.RoutedCommand" Value="F4"/> **** ?? ****
<Setter Property="beh:RoutedCommandWire.ICommand" Value="{Binding CloseCommand}"/>
</Style>
Here is what I think the answer code looks like in C#
public class RoutedCommandWire
{
public static readonly DependencyProperty RoutedCommandProperty =
DependencyProperty.RegisterAttached("RoutedCommand", typeof(RoutedCommand), typeof(RoutedCommandWire), new PropertyMetadata(OnCommandChanged));
public static RoutedCommand GetRoutedCommand(DependencyObject d) { return (RoutedCommand) d.GetValue(RoutedCommandProperty); }
public static void SetRoutedCommand(DependencyObject d, RoutedCommand value) { d.SetValue(RoutedCommandProperty, value); }
public static readonly DependencyProperty ICommandProperty =
DependencyProperty.RegisterAttached("Iommand", typeof(ICommand), typeof(RoutedCommandWire));
public static ICommand GetICommand(DependencyObject d) { return (ICommand) d.GetValue(ICommandProperty); }
public static void SetICommand(DependencyObject d, ICommand value) { d.SetValue(ICommandProperty, value); }
private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var fe = d as FrameworkElement;
if(fe==null) return;
if (e.OldValue != null) {
Detach(fe, (RoutedCommand) e.OldValue);
}
if (e.NewValue != null) {
Attach(fe, (RoutedCommand) e.NewValue, Execute, CanExecute);
}
}
private static void CanExecute(object sender, CanExecuteRoutedEventArgs e) {
var depObj = sender as DependencyObject;
if (depObj == null) return;
var command = GetICommand(depObj);
if (command == null) return;
e.CanExecute = command.CanExecute(e.Parameter);
e.Handled = true;
}
private static void Execute(object sender, ExecutedRoutedEventArgs e)
{
var depObj = sender as DependencyObject;
if (depObj == null) return;
var command = GetICommand(depObj);
if (command == null) return;
command.Execute(e.Parameter);
e.Handled = true;
}
public static void Detach(FrameworkElement fe, RoutedCommand command) {
var bindingCollection = fe.CommandBindings;
if (bindingCollection.Count == 0) return;
var matches = bindingCollection.Cast<CommandBinding>().Where(binding => binding.Equals(command));
foreach (var binding in matches) {
bindingCollection.Remove(binding);
}
}
public static void Attach(FrameworkElement fe, RoutedCommand command,
ExecutedRoutedEventHandler executedHandler, CanExecuteRoutedEventHandler canExecuteHandler, bool preview = false)
{
if (command == null || executedHandler == null) return;
var binding = new CommandBinding(command);
if (preview)
{
binding.PreviewExecuted += executedHandler;
if (canExecuteHandler != null)
{
binding.PreviewCanExecute += canExecuteHandler;
}
}
else
{
binding.Executed += executedHandler;
if (canExecuteHandler != null)
{
binding.CanExecute += canExecuteHandler;
}
}
fe.CommandBindings.Add(binding);
}
}

KeyBindings work only on controls which accept keyboard input. A Border doesn't. In general, InputBindings are also different from CommandBindings in that you can define a CommandBinding on a parent element so it handles commands when child elements have focus, but you can't define InputBindings on parent elements in order to have them effective on the child elements.
What you can do is to add a default InputGesture to your command's InputGestures collection. That seems to make the command available using that keyboard shortcut from every control that accepts keyboard input (that's much better than to have to specify InputBindings everywhere, isn't it?). In order to take advantage of this, you would have to use a RoutedCommand to invoke your MVVM-ICommand. You can combine the two using attached properties, in a pattern which I call "sticky command" and which is very similar to an attached behaviour.
This code defines the attached properties:
Public Class Close
Public Shared ReadOnly CommandProperty As DependencyProperty = DependencyProperty.RegisterAttached("Command", GetType(RoutedCommand), GetType(Close), New PropertyMetadata(AddressOf OnCommandChanged))
Public Shared Function GetCommand(ByVal d As DependencyObject) As RoutedCommand
Return d.GetValue(CommandProperty)
End Function
Public Shared Sub SetCommand(ByVal d As DependencyObject, ByVal value As RoutedCommand)
d.SetValue(CommandProperty, value)
End Sub
Public Shared ReadOnly MVVMCommandProperty As DependencyProperty = DependencyProperty.RegisterAttached("MVVMCommand", GetType(ICommand), GetType(Close))
Public Shared Function GetMVVMCommand(ByVal d As DependencyObject) As ICommand
Return d.GetValue(MVVMCommandProperty)
End Function
Public Shared Sub SetMVVMCommand(ByVal d As DependencyObject, ByVal value As ICommand)
d.SetValue(MVVMCommandProperty, value)
End Sub
Private Shared Sub OnCommandChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
If e.OldValue IsNot Nothing Then
Detach(d, DirectCast(e.OldValue, RoutedCommand))
End If
If e.NewValue IsNot Nothing Then
Attach(d, DirectCast(e.NewValue, RoutedCommand), AddressOf DoCloseCommand, AddressOf CanDoCloseCommand)
End If
End Sub
Private Shared Sub CanDoCloseCommand(ByVal sender As Object, ByVal e As CanExecuteRoutedEventArgs)
If sender IsNot Nothing Then
Dim com As ICommand = GetMVVMCommand(sender)
If com IsNot Nothing Then
e.CanExecute = com.CanExecute(e.Parameter)
e.Handled = True
End If
End If
End Sub
Private Shared Sub DoCloseCommand(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)
If sender IsNot Nothing Then
Dim com As ICommand = GetMVVMCommand(sender)
If com IsNot Nothing Then
com.Execute(e.Parameter)
e.Handled = True
End If
End If
End Sub
Public Shared Sub Detach(ByVal base As FrameworkElement, ByVal command As RoutedCommand)
Dim commandBindings As CommandBindingCollection = base.CommandBindings
If commandBindings IsNot Nothing Then
Dim bindings = From c As CommandBinding In commandBindings
Where c.Command Is command
Select c
Dim bindingList As New List(Of CommandBinding)(bindings)
For Each c As CommandBinding In bindingList
commandBindings.Remove(c)
Next
End If
End Sub
Public Shared Sub Attach(ByVal base As FrameworkElement, ByVal command As RoutedCommand, ByVal executedHandler As ExecutedRoutedEventHandler, ByVal canExecuteHandler As CanExecuteRoutedEventHandler, Optional ByVal preview As Boolean = False)
If command IsNot Nothing And executedHandler IsNot Nothing Then
Dim b As CommandBinding = New CommandBinding(command)
If preview Then
AddHandler b.PreviewExecuted, executedHandler
If canExecuteHandler IsNot Nothing Then
AddHandler b.PreviewCanExecute, canExecuteHandler
End If
Else
AddHandler b.Executed, executedHandler
If canExecuteHandler IsNot Nothing Then
AddHandler b.CanExecute, canExecuteHandler
End If
End If
base.CommandBindings.Add(b)
'For Each i As InputGesture In command.InputGestures
' GetInputBindings(base).Add(New InputBinding(command, i))
'Next
End If
End Sub
You'd use both of them on your TabItems, I guess, since that is where you want to handle the close command, and you would set Close.Command to a RoutedCommand which has the keyboard shortcut in its InputGestures, and Close.MVVMCommand="{Binding CloseCommand}".
UPDATE
You can define a RoutedCommand like this in your ViewModel:
Public Shared ReadOnly TestCommand As New RoutedUICommand("Test", "TestCommand", GetType(ViewModel))
Shared Sub New()
TestCommand.InputGestures.Add(New KeyGesture(Key.T, ModifierKeys.Control))
End Sub
The static constructor sets the default keygesture for the command. If you want to do that in XAML, you could also do that using custom attached properties. Anyway, you'd reference the RoutedCommand like this in XAML:
Close.Command="{x:Static my:ViewModel.TestCommand}"

Related

WPF DataGrid - Virtualised - select all

With a WPF virtualised DataGrid I need to employ a 'select all' method. Iterating though the grid itself won't work as it will only select the records that at visible in the UI - but of course if they are selected using the mouse they are retained. I'm guessing that something must change in the DataTable, but what? If so can it be manipulated so that all the records are selected in the DataGrid?
Thanks
======================== ADDED===========================
Eran - thanks for your reply but having a job wiring this up
Create DataGrid
Dim DGV As New CustomControl.DGVx
With DGV
.Name = "Invoice_AdHoc_DGV"
.AutoGenerateColumns = False
.SelectionMode = SelectionMode.Multiple
End With
RegisterControl(Invoice_AdHoc_Grid, DGV)
RightGrid.Children.Add(DGV)
Bind to DataTable
DGV.ItemsSource = AdHocDT.DefaultView
Created the class from your answer
Public Class ObjectSelectAll
Implements System.ComponentModel.INotifyPropertyChanged
Public Property Name() As String
Private vIsSelected As Boolean
Public Property IsSelected() As Boolean
Get
Return vIsSelected
End Get
Set(value As Boolean)
vIsSelected = value
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs("IsSelected"))
End Set
End Property
Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class
cs :
private List<MyObject> _items;
public List<MyObject> Items
{
get
{
if(_items == null)
_items = new List<MyObject> { new MyObject { Name = "obj1" }, new MyObject { Name = "obj2" }, new MyObject { Name = "obj3" }, new MyObject { Name = "obj4" } };
return _items;
}
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
foreach (var item in Items)
{
item.IsSelected = true;
}
}
}
public class MyObject : INotifyPropertyChanged
{
public string Name { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
xaml :
<Button Click="Button_Click_1" Content="Select All"/>
<DataGrid Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding Items}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"/>
</DataGrid.Columns>
</DataGrid>
Finally managed to resolve this issue
Added another column to the DataTable
With AdHocDT.Columns
.Add("Selected", GetType(Boolean))
End With
When 'Select All' is clicked set this to True for all rows and in the Grid the value is updated to true as well, then add this to ensure that the Grid shows it as selected when the virtual data is scrolled into view
Private Sub Invoice_AdHoc_DGV_ItemsChanged(sender As Object, e As EventArgs)
Try
Dim DGV As CustomControl.DGVx = Invoice_AdHoc_Grid.FindName("Invoice_AdHoc_DGV")
Dim i As Integer = 0
For Each Row As DataRowView In DGV.Items
Dim vRow As DevComponents.WPF.Controls.AdvGridRow = CType(DGV.ItemContainerManager.ContainerFromItem(DGV.Items(i), False), DevComponents.WPF.Controls.AdvGridRow)
If Not vRow Is Nothing Then
If Row("Selected") = True Then
vRow.IsSelected = True
End If
End If
i += 1
Next
Catch ex As Exception
EmailError(ex)
End Try
End Sub
and handle the event
AddHandler DGV.ItemContainerManager.VisibleContainersChanged, AddressOf Invoice_AdHoc_DGV_ItemsChanged
Now the Grid shows all the rows as selected :-)

Textbox in listview - update source and move focus on tab not working at same time

I have a listview. I have set following int that :-
<ListView KeyboardNavigation.TabNavigation="Local" SelectionMode="Extended">
<ListView.ItemContainerStyle>
<Style>
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</ListView.ItemContainerStyle>
One column in listview contains TextBox's.
If I set the UpdateSourceTrigger=LostFocus
in my textbox, I can not tab through the listview...Instead if I set UpdateSourceTrigger=Explicit, the tabbing is working...but source will not get updated.
Please help me
EDIT
public class TextBoxBehavior
{
#region Attached Property EscapeClearsText
public static readonly DependencyProperty EscapeClearsTextProperty
= DependencyProperty.RegisterAttached("EscapeClearsText", typeof(bool), typeof(TextBoxBehavior),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEscapeClearsTextChanged)));
private static void OnEscapeClearsTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.KeyUp -= TextBoxKeyUp;
textBox.KeyUp += TextBoxKeyUp;
}
}
}
private static void TextBoxKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
//((DataContext<string>)((TextBox)sender).GetBindingExpression(TextBox.TextProperty).DataItem).RollbackChanges();
((TextBox)sender).Text = string.Empty;
}
else if (e.Key == Key.Enter)
{
((TextBox)sender).GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
public static void SetEscapeClearsText(DependencyObject dependencyObject, bool escapeClearsText)
{
if (!ReferenceEquals(null, dependencyObject))
dependencyObject.SetValue(EscapeClearsTextProperty, escapeClearsText);
}
public static bool GetEscapeClearsText(DependencyObject dependencyObject)
{
if (!ReferenceEquals(null, dependencyObject))
return (bool)dependencyObject.GetValue(EscapeClearsTextProperty);
return false;
}
#endregion Attached Property EscapeClearsText
}
Below is the listview/gridview column which has the attached property in it.
<GridViewColumn Width="60">
<GridViewColumnHeader Content="Priority"
Command="{Binding Path=SortSelectedClaimCodeGroupsCommand}"
CommandParameter="Item.IntPriority">
</GridViewColumnHeader>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border DataContext="{Binding Item.Priority}"
Style="{StaticResource ValidationResultBorderStyle}" HorizontalAlignment="Left" >
<TextBox Width="200" MaxLength="25" Text="{Binding Path=Value,Mode=TwoWay,
UpdateSourceTrigger=Explicit}" local:TextBoxBehavior.EscapeClearsText="True" >
When you set the UpdateSourceTrigger as explicit, you have to update the source by explicitly calling the method UpdateSource on your BindingExpression. Where is the code for that?
EDIT
In your TextBoxKeyUp event you are overwriting your Binding by setting the text on the press of Escape key. Firstly you bind it to the property Value and later you are explicitly setting the Textbox text property to String.Empty.This way text property will loose it's binding. So, later whenever you call UpdateSource it won't propagate to Source value since it's no longer binded to the Text property of textbox. Instead you should set the text like this -
((TextBox)sender).SetCurrentValue(TextBox.TextProperty, String.Empty);
This way your binding will be preserved and UpdateSource would work as it should.

Disable WPF TreeView (or TreeViewItem) selection?

Is there a nice way (except retemplating the whole TreeViewItem.Template) to disable selection in TreeView?
I am basically looking for the ItemsControl style of the TreeView (An ItemsControl is the best use to 'disable' selection on ListBox, read this post)
Try this:
<Trigger Property="HasItems" Value="true">
<Setter Property="Focusable" Value="false" />
</Trigger>
This did the trick for me (based on this answer, but no tied to item - selection is disabled whatsoever):
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Focusable" Value="False" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Based off of the links to the currently accepted answer, I implemented this in my project:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListView.ItemContainerStyle>
Works for TreeViewItem as well. And in the view model:
protected bool _DisableSelection;
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
if (value == _IsSelected) return;
_IsSelected = _DisableSelection ? false : value;
NotifyPropertyChanged();
}
}
Now you don't have to go hunting!
Whenever an item is selected, you could "unselect" it. Ex. modify the code from http://www.codeproject.com/KB/WPF/TreeView_SelectionWPF.aspx or use a MVVM approach (see http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx) and always set IsSelected back to false.
I decided to write a reusable behavior, HTH:
Namespace Components
Public NotInheritable Class TreeViewBehavior
Public Shared Function GetIsTransparent(
ByVal element As TreeViewItem) As Boolean
If element Is Nothing Then Throw New ArgumentNullException("element")
Return element.GetValue(IsTransparentProperty)
End Function
Public Shared Sub SetIsTransparent(ByVal element As TreeViewItem,
ByVal value As Boolean)
If element Is Nothing Then Throw New ArgumentNullException("element")
element.SetValue(IsTransparentProperty, value)
End Sub
Public Shared ReadOnly IsTransparentProperty As DependencyProperty =
DependencyProperty.RegisterAttached("IsTransparent", GetType(Boolean),
GetType(TreeViewBehavior),
New FrameworkPropertyMetadata(False,
AddressOf IsTransparent_PropertyChanged))
Private Shared Sub IsTransparent_PropertyChanged(
ByVal sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
Dim tvi = DirectCast(sender, TreeViewItem)
Dim isTransparent = CBool(e.NewValue)
If isTransparent Then
AddHandler tvi.Selected, AddressOf tvi_Selected
Else
RemoveHandler tvi.Selected, AddressOf tvi_Selected
End If
End Sub
Private Shared Sub tvi_Selected(ByVal sender As Object,
ByVal e As RoutedEventArgs)
Dim treeViewItem = DirectCast(sender, TreeViewItem)
If Not treeViewItem.IsSelected Then Exit Sub
treeViewItem.Dispatcher.Invoke(
Sub(tvi As TreeViewItem) tvi.IsSelected = False,
System.Windows.Threading.DispatcherPriority.Send,
treeViewItem)
End Sub
End Class
End Namespace
Usage:
<Window xmlns:components="clr-namespace:WpfApplication.Components">
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter
Property="components:TreeViewBehavior.IsTransparent"
Value="True" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Window>
I tried this and it worked for me. Because I have a simple and not dynamic treeview. But I think it can work by putting it in a style
<TreeViewItem ... Focusable="False" IsSelected="False"/>
I just unselected the TreeViewItems as they get selected.
I use TreeView only once. However if I added it in several places I would consider looking in to adding this to a Attached Behavior.
Xaml:
<TreeView SelectedItemChanged="TreeView_SelectionChanged">
Code behind:
private void TreeView_SelectionChanged(object sender, RoutedEventArgs e)
{
if (!(sender is TreeView myTreeView)) return;
var selectedItem = (TreeViewItem)myTreeView.SelectedItem;
if (selectedItem == null) return;
selectedItem.IsSelected = false;
}
I did this a differently than the accepted answer:
Lets say that you have a property in your ViewModel (say 'ShouldPreventSelection')
Now when ShouldPreventSelection is true you want selection to be disabled:
In your TreeView fire the PreviewSelected event like so:
<TreeView Name="TreeView1"
...
PreviewSelected="TreeView1_PreviewSelected"
..
/>
Then in the codebehind you can the following:
private void TreeView1_PreviewSelected(object sender, RoutedEventArgs e)
{
MyViewModel myViewModel = TreeView1.DataContext as MyViewModel;
if (myViewModel == null)
{
return;
}
if (myViewModel .ShouldPreventSelection)
{
e.Handled = true;
}
}

WPF Toolkit DataGrid Multi-Select: How to get SelectedItems out?

I'm using the WPF Toolkit's DataGrid. I've enabled the property on the DataGrid to allow for multi-selecting of rows. How do I get the SelectedItems out? I'm using an MVVM framework to bind my ViewModel to my View.
Thanks!
Taking Bill's answer, merging options 1 and 2, adding a sprinkling of attached properties as an alternative to writing code-behind, I came up with a Behavior.
Firstly, we have the behavior:
Public Class SelectedItemsBehavior
Public Shared ReadOnly SelectedItemsChangedHandlerProperty As DependencyProperty =
DependencyProperty.RegisterAttached("SelectedItemsChangedHandler",
GetType(mvvm.RelayCommand), GetType(SelectedItemsBehavior),
New FrameworkPropertyMetadata(New PropertyChangedCallback(AddressOf OnSelectedItemsChangedHandlerChanged)))
Public Shared Function GetSelectedItemsChangedHandler(ByVal element As DependencyObject) As mvvm.RelayCommand
If element Is Nothing Then Throw New ArgumentNullException("element")
Return element.GetValue(SelectedItemsChangedHandlerProperty)
End Function
Public Shared Sub SetSelectedItemsChangedHandler(ByVal element As DependencyObject, ByVal value As mvvm.RelayCommand)
If element Is Nothing Then Throw New ArgumentNullException("element")
element.SetValue(SelectedItemsChangedHandlerProperty, value)
End Sub
Private Shared Sub OnSelectedItemsChangedHandlerChanged(d As DependencyObject,
e As DependencyPropertyChangedEventArgs)
Dim dataGrid As DataGrid = DirectCast(d, DataGrid)
If e.OldValue Is Nothing AndAlso e.NewValue IsNot Nothing Then
AddHandler dataGrid.SelectionChanged, AddressOf ItemsControl_SelectionChanged
End If
If e.OldValue IsNot Nothing AndAlso e.NewValue Is Nothing Then
RemoveHandler dataGrid.SelectionChanged, AddressOf ItemsControl_SelectionChanged
End If
End Sub
Public Shared Sub ItemsControl_SelectionChanged(sender As Object,
e As SelectionChangedEventArgs)
Dim dataGrid As DataGrid = DirectCast(sender, DataGrid)
Dim itemsChangedHandler As RelayCommand = GetSelectedItemsChangedHandler(dataGrid)
itemsChangedHandler.Execute(dataGrid.SelectedItems)
End Sub
End Class
C#:
using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
public class SelectedItemsBehavior
{
public static readonly DependencyProperty SelectedItemsChangedHandlerProperty = DependencyProperty.RegisterAttached("SelectedItemsChangedHandler", typeof(mvvm.RelayCommand), typeof(SelectedItemsBehavior), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnSelectedItemsChangedHandlerChanged)));
public static mvvm.RelayCommand GetSelectedItemsChangedHandler(DependencyObject element)
{
if (element == null)
throw new ArgumentNullException("element");
return element.GetValue(SelectedItemsChangedHandlerProperty);
}
public static void SetSelectedItemsChangedHandler(DependencyObject element, mvvm.RelayCommand value)
{
if (element == null)
throw new ArgumentNullException("element");
element.SetValue(SelectedItemsChangedHandlerProperty, value);
}
private static void OnSelectedItemsChangedHandlerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = (DataGrid)d;
if (e.OldValue == null && e.NewValue != null) {
dataGrid.SelectionChanged += ItemsControl_SelectionChanged;
}
if (e.OldValue != null && e.NewValue == null) {
dataGrid.SelectionChanged -= ItemsControl_SelectionChanged;
}
}
public static void ItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DataGrid dataGrid = (DataGrid)sender;
RelayCommand itemsChangedHandler = GetSelectedItemsChangedHandler(dataGrid);
itemsChangedHandler.Execute(dataGrid.SelectedItems);
}
}
Then we add it to the datagrid in XAML:
<DataGrid AutoGenerateColumns="False" FontFamily="Tahoma" FontSize="9"
ItemsSource="{Binding Path=ResultsVM}"
mvvm:SelectedItemsBehavior.SelectedItemsChangedHandler="{Binding Path=ResultsSelectionChangedCommand}" />
Then we code the RelayCommand in the ViewModel:
Public ReadOnly Property ResultsSelectionChangedCommand() As mvvm.RelayCommand
Get
If _resultsSelectionChangedCommand Is Nothing Then
_resultsSelectionChangedCommand = New mvvm.RelayCommand(AddressOf ResultsSelectionChanged)
End If
Return _resultsSelectionChangedCommand
End Get
End Property
Public Sub ResultsSelectionChanged(ByVal selectedItems As Object)
_resultsSelectedItems.Clear()
For Each item In selectedItems
_resultsSelectedItems.Add(item)
Next
End Sub
C#:
public mvvm.RelayCommand ResultsSelectionChangedCommand {
get {
if (_resultsSelectionChangedCommand == null) {
_resultsSelectionChangedCommand = new mvvm.RelayCommand(ResultsSelectionChanged);
}
return _resultsSelectionChangedCommand;
}
}
public void ResultsSelectionChanged(object selectedItems)
{
_resultsSelectedItems.Clear();
foreach (item in selectedItems) {
_resultsSelectedItems.Add(item);
}
}
The _resultsSelectedItems is simply a list of items displayed in the DataGrid:
Private _resultsSelectedItems As New List(Of WorkOrderMatchViewModel)
C#:
private List<WorkOrderMatchViewModel> _resultsSelectedItems = new List<WorkOrderMatchViewModel>();
Hope this helps, kinda uses both of Bill's methods without having to reference MVVM-Light.
I've been looking for an answer to this question as well. The answers that I have found is either to
1) in the codebehind delegate the work to a method in the ViewModel passing the SelectedItems list from the datagrid. This collection will contain all of the items that are selected.
Or
2) use the MVVM toolkit light that allows you to use Event to Command and pass an object as a parameter directly to the ViewModel.
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
IList lst = this.myDataGrid.SelectedItems;
ViewModel.RowsSelected(lst);
}
In this case you will need to bind your SelectionChanged in your xaml to your selectionchanged in the code behind. Then in your code-behind you can save this list and use it for deleting selected rows, etc.
If there is a better way to do this, I'd love to know is well.
HTH
Bill
C# version SelectedItemsBehavior class. May be help someone.
public static class SelectedItemsBehavior
{
public static readonly DependencyProperty SelectedItemsChangedHandlerProperty =
DependencyProperty.RegisterAttached("SelectedItemsChangedHandler",
typeof(RelayCommand),
typeof(SelectedItemsBehavior),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnSelectedItemsChangedHandlerChanged)));
public static RelayCommand GetSelectedItemsChangedHandler(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return element.GetValue(SelectedItemsChangedHandlerProperty) as RelayCommand;
}
public static void SetSelectedItemsChangedHandler(DependencyObject element, RelayCommand value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(SelectedItemsChangedHandlerProperty, value);
}
public static void OnSelectedItemsChangedHandlerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = (DataGrid)d;
if (e.OldValue == null && e.NewValue != null)
{
dataGrid.SelectionChanged += new SelectionChangedEventHandler(ItemsControl_SelectionChanged);
}
if (e.OldValue != null && e.NewValue == null)
{
dataGrid.SelectionChanged -= new SelectionChangedEventHandler(ItemsControl_SelectionChanged);
}
}
public static void ItemsControl_SelectionChanged(Object sender, SelectionChangedEventArgs e)
{
DataGrid dataGrid = (DataGrid)sender;
RelayCommand itemsChangedHandler = GetSelectedItemsChangedHandler(dataGrid);
itemsChangedHandler.Execute(dataGrid.SelectedItems);
}
}
I managed to get around this using Relay Commands as Bill mentioned. It is a little dirty in parts, but I avoided putting any code in the behind file.
Firstly, in your XAML - Bind your command onto a button, or whatever triggers your RelayCommand.
<Button Content="Select"
cmd:ButtonBaseExtensions.Command="{Binding CommandSelect}"
cmd:ButtonBaseExtensions.CommandParameter="{Binding ElementName=Results, Path=SelectedItems}" />
You'll notice the command parameter Binds to another UI element - the DataGrid or ListView you wish to get the selected items of. This syntax will work in Silverlight 3 as well as WPF, as it now supports element to element binding.
In your view model your Command will look something like this
Private _CommandSelect As RelayCommand(Of IEnumerable)
Public ReadOnly Property CommandSelect() As RelayCommand(Of IEnumerable)
Get
If _CommandSelect Is Nothing Then
_CommandSelect = New RelayCommand(Of IEnumerable)(AddressOf CommandSelectExecuted, AddressOf CommandSelectCanExecute)
End If
Return _CommandSelect
End Get
End Property
Private Function CommandSelectExecuted(ByVal parameter As IEnumerable) As Boolean
For Each Item As IElectoralAreaNode In parameter
Next
Return True
End Function
Private Function CommandSelectCanExecute() As Boolean
Return True
End Function
The Selected Items will be returned as a SelectedItemCollection, but you probably don't want this dependancy in your View Model. So typing it as IEnumerable and doing a little casting is your only option, hense the 'dirtyness'. But it keeps your code behind clean and MVVM pattern in tact!

DataGridTextColumn.MaxLength?

How do I set the MaxLength property of the DataGridTextColumn?
<tk:DataGridTextColumn Binding="{Binding Text}">
<tk:DataGridTextColumn.EditingElementStyle>
<Style TargetType="TextBox">
<Setter Property="MaxLength" Value="16"/>
</Style>
</tk:DataGridTextColumn.EditingElementStyle>
</tk:DataGridTextColumn>
You could also set it using the following behavior so you don't have to use a style and setters each time:
Public Class TextBoxBehavior
Private Shared Types As Type() = New Type() {GetType(AutoCompleteBox), GetType(ComboBox), GetType(DataGridTextColumn)}
Public Shared Function GetMaxLength(ByVal element As DependencyObject) As Integer
Return element.GetValue(MaxLengthProperty)
End Function
Public Shared Sub SetMaxLength(ByVal element As DependencyObject, ByVal value As Integer)
element.SetValue(MaxLengthProperty, value)
End Sub
Private Shared Sub ValidateElement(ByVal element As DependencyObject)
If element Is Nothing Then Throw New ArgumentNullException("element")
If Not Types.Contains(element.GetType) Then Throw New NotSupportedException("The TextBoxBehavior is not supported for the given element")
End Sub
Public Shared ReadOnly MaxLengthProperty As DependencyProperty =
DependencyProperty.RegisterAttached("MaxLength",
GetType(Integer), GetType(TextBoxBehavior),
New FrameworkPropertyMetadata(Integer.MaxValue, AddressOf TextBox_MaxLengthChanged))
Private Shared Sub TextBox_MaxLengthChanged(ByVal sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
If sender Is Nothing Then Exit Sub
Dim value = DirectCast(e.NewValue, Integer)
If TypeOf sender Is AutoCompleteBox Then
Dim acb = DirectCast(sender, AutoCompleteBox)
If acb.IsLoaded Then
Dim tb = DirectCast(acb.Template.FindName("Text", acb), TextBox)
tb.MaxLength = value
Else
acb.AddHandler(AutoCompleteBox.LoadedEvent, New RoutedEventHandler(AddressOf Element_Loaded))
End If
ElseIf TypeOf sender Is ComboBox Then
Dim cb = DirectCast(sender, ComboBox)
If cb.IsLoaded Then
Dim tb = DirectCast(cb.Template.FindName("PART_EditableTextBox", cb), TextBox)
tb.MaxLength = value
Else
cb.AddHandler(ComboBox.LoadedEvent, New RoutedEventHandler(AddressOf Element_Loaded))
End If
ElseIf TypeOf sender Is DataGridTextColumn Then
Dim dgtc = DirectCast(sender, DataGridTextColumn)
Dim setter = GetIsMaxLengthSet(dgtc.EditingElementStyle)
If setter Is Nothing Then
Dim style = New Style(GetType(TextBox), dgtc.EditingElementStyle)
style.Setters.Add(New Setter(TextBox.MaxLengthProperty, value))
dgtc.EditingElementStyle = style
style.Seal()
Else
setter.Value = value
End If
End If
End Sub
Private Shared Function GetIsMaxLengthSet(ByVal style As Style) As Setter
If style Is Nothing Then Return Nothing
Dim setter = style.Setters.LastOrDefault(Function(s) TypeOf s Is Setter AndAlso DirectCast(s, Setter).Property Is TextBox.MaxLengthProperty)
If setter IsNot Nothing Then Return setter Else Return GetIsMaxLengthSet(style.BasedOn)
End Function
Private Shared Sub Element_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim ml = GetMaxLength(sender)
TextBox_MaxLengthChanged(sender, New DependencyPropertyChangedEventArgs(TextBox.MaxLengthProperty, -1, ml))
sender.RemoveHandler(FrameworkElement.LoadedEvent, New RoutedEventHandler(AddressOf Element_Loaded))
End Sub
End Class
Usage:
<ComboBox xmlns:loc="MyNamesapace" loc:TextBoxBehavior.MaxLength="50" />
If you have a shared style among all columns, and you would like to add an additional style to one or more of those, you could use the Style.BasedOn Property:
<DataGridTextColumn Binding="{Binding SomeProperty, UpdateSourceTrigger=PropertyChanged}" ElementStyle="{StaticResource CellErrorStyle}">
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="TextBox" BasedOn="{StaticResource OriginalStyleKey}">
<Setter Property="MaxLength" Value="5" />
</Style>
</DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>
<Window.Resources>
<Style x:Key="sty_txtDesc" TargetType="TextBox">
<Setter Property="MaxLength" Value="495" />
</Style>
</Window.Resources>
for (int i = 0; i < miDataGridX.Columns.Count; i++)
{
if (miDataGridX.Columns[i].Header.ToString() == "Description")
{
((DataGridTextColumn)miDataGridX.Columns[i]).EditingElementStyle = (Style)this.FindResource("sty_txtDesc");
}
}

Resources