WPF Datagrid Items Refresh lose focus - wpf

Refreshing my datagrid when my observableCollection gets updated in the viewmodel have been a nightmare. After I discover the DataGrid won't respond to the events raised by the ObservableCollection I discovered DataGrid.Items.Refresh.
It does refresh but then the DataGrid loses focus. I have a simple list and I want to change a value when I press a key and then update. Its unacceptable the user have to pick the mouse again when using keyboard shortcuts ...
Here is a simple example:
<DataGrid x:Name="MyDataGrid" SelectionMode="Single" AutoGenerateColumns="False" IsReadOnly="True" KeyUp="MyDataGrid_KeyUp">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding Path=First}"/>
<DataGridTextColumn Header="Last Name" Binding="{Binding Path=Last}"/>
</DataGrid.Columns>
</DataGrid>
And the code behind:
private void MyDataGrid_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key.Equals(Key.Space))
{
MyDataGrid.Items.Refresh();
}
}
p.s. In this example I'm setting the ItemsSource in my code behind and not binding to a ObservableCollection. Also i'm using just the codebehind and not a ViewModel but the problem is the same.
edit: The initial problem was that I wasnt using the NotifyPropertyChanged in my class. However, the problem here presented is still "open", I can't really understand the lost focus question when I do the Refresh()

Refreshing my datagrid when my observableCollection gets updated in the viewmodel have been a nightmare. - Why has this been a nightmare? Should be easy though.
Regarding your problem. Please try the following
private void MyDataGrid_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key.Equals(Key.Space))
{
MyDataGrid.Items.Refresh();
MyDataGrid.Focus();
}
}
You can find the related doc here.
Edit
Let's try this one
private void MyDataGrid_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key.Equals(Key.Space))
{
MyDataGrid.Items.Refresh();
FocusManager.SetFocusedElement(MyDataGrid);
}
}
For more information, please have a look here.

Scheduling the refresh via dispatcher worked for me (with a TreeView).
So instead of doing this (loses focus):
tree.Items.Refresh();
I do this (does not lose focus):
Dispatcher.BeginInvoke(new Action(() => tree.Items.Refresh()));
No idea why but it works for me.

Related

WPF event unsubscribe leak in DataTemplate (XAML) on NET Framework

Imagine a UserControl with a ListBox having a CheckBox in a DataTemplate. The ItemsSource for the ListBox is some global list. The CheckBox has Checked/Unchecked events attached to it.
<ListBox ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type a:Data}">
<CheckBox Content="{Binding Path=Name}"
Checked="ToggleButton_OnChecked"
Unchecked="ToggleButton_OnUnchecked"
IsChecked="{Binding Path=IsEnabled}"
Padding="10"
VerticalContentAlignment="Center"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I am logging loaded/unloaded/checked/unchecked events in the main window's log.
private void ToggleButton_OnChecked(object sender, RoutedEventArgs e)
{
Log("Checked");
}
private void ToggleButton_OnUnchecked(object sender, RoutedEventArgs e)
{
Log("Unchecked");
}
private void UserControl1_OnLoaded(object sender, RoutedEventArgs e)
{
Log("Loaded");
}
private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
{
Log("Unloaded");
}
The main window features a dynamic list of UserControl1 instances (starting with just one). There are add/remove buttons that allow me to add more instances.
<UniformGrid Rows="2">
<DockPanel>
<Button DockPanel.Dock="Top" Click="Add">Add</Button>
<Button DockPanel.Dock="Top" Click="Remove">Remove</Button>
<ListBox x:Name="ListBox">
<local:UserControl1 />
</ListBox>
</DockPanel>
<ListBox ItemsSource="{Binding ElementName=This,Path=Log}" FontFamily="Courier New"/>
</UniformGrid>
The window's codebehind:
private void Add(object sender, RoutedEventArgs e)
{
ListBox.Items.Add(new UserControl1());
}
private void Remove(object sender, RoutedEventArgs e)
{
if (ListBox.Items.Count == 0) return;
ListBox.Items.RemoveAt(0);
}
When I run the app there is just one UserControl1 instance. If I add one more and then immediately remove one of them, then click the one and only checkbox on the screen, I see two "Checked" events logged. If I now uncheck it, there are two "Unchecked" events (even though "Unloaded" event was previously clearly logged. The hex numbers on the left show the output of a GetHashCode() which clearly shows the events were handled by distinct UserControl1 instances.
So even though one of UserControl1 gets unloaded, the events don't seem to get unsubscribed automatically. I have tried upgrading to NET Framework 4.8 to no avail. I see the same behavior. If I add 10 new controls and remove them immediately, I will observe 10 "Checked" or "Unchecked" events.
I have tried searching for a similar problem but could not find it. Is there something I am missing or did I just encounter a bug? Looking for workarounds.
Full source code is available on GitHub. https://github.com/wpfwannabe/datacontext-event-leak
In the MVVM pattern, the view is bind to the view model and doesn't survive it when the garbage collection do it's job.
In the example you provided, the view model is a static object and by definition can't be garbage collected, so the view can't neither be garbage collected.
There is no automatic unbinding since you can reuse an instance of an user control (it can be Loaded and UnLoaded multiple times).
The simplest¹ way to fix this memory leak is to do the unbinding on unload :
Wpf
// First give the ListBox a name
<ListBox x:Name="ListBox" ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">
Code behind
private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
{
Log("Unloaded");
ListBox.ItemsSource = null;
}
1: The proper way is to wrap the static list in a dedicated list view model, make the view model disposable (to unbind the wrapper from the static list on dispose), dispose the view model on remove.

DataGrid bind command to cell click

Here's a quick question. I have a DataGrid that looks like this:
<DataGrid ItemsSource="{Binding Path=Sections}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ImportName, Mode=OneWay}" Header="Imported" />
<DataGridTextColumn Binding="{Binding Path=FoundName, Mode=TwoWay}" Header="Suggested" />
</DataGrid.Columns>
</DataGrid>
I want to bind "Suggested" column cells to a command in my VM, so that each time user clicks the cell for editing, my command would execute and show a dialog for the user. I've found an interesting solution to a similar problem described here: DataGrid bind command to row select
I like the fact that it manages this from XAML without any code-behind that attaches to the cell editing event. Unfortunately, I've no idea how to convert it in a way that would allow me to bind command to cells in a specific column, rather than the entire row. Any advice in regards to that?
You can use BeginningEdit event in the DataGrid control to handle this scenario. This event will fires before a row or cell enters edit mode. You can identify the selected column from the EventArgs.
Example:
private void dgName_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (e.Column.Header.ToString() == "Suggested")
{
//Do Operation
}
}
If you are using MVVM pattern, there are options to pass EventArgs to VM. If you are uusing MVVMLight Toolkit, there is an option called PassEventArgs and set it to TRUE.
In VM,
//Relay Command
private RelayCommand<DataGridBeginningEditEventArgs> _cellBeginningEditCommand;
public RelayCommand<DataGridBeginningEditEventArgs> CellBeginningEditCommand
{
get
{
return _cellBeginningEditCommand ?? (_cellBeginningEditCommand = new RelayCommand<DataGridBeginningEditEventArgs>(CellBeginningEditMethod));
}
}
//Command Handler
private void CellBeginningEditMethod(DataGridBeginningEditEventArgs args)
{
if(args.Column.Header.ToString() == "Suggested")
{
//Do Operation
}
}

How can I perform a custom action on the data of a DataGridRow when it is selected?

I've been trying to figure out how to get this custom behaviour into a datagrid with out having much look when searching online for solutions.
Given the following datagrid (some xaml removed for brevity):
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="auto">
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
I have the checkbox successfully bound to the databound object for each row. (Note: I'm using a DataGridTemplateColumn rather than DataGridCheckBoxColumn so that you do not need to double-click to change the value).
What I would like to achieve is to have the ability to tick the checkbox / update the Selected property of the databound object when the user selects a row. Effectively making the entire row click set the checked property of the checkbox. Ideally, I'd like to do this without a code behind file if possible as I'm trying to keep my code behinds as clean as possible.
Another feature which I would like, if possible, would be that clicking on a row would toggle it's selected property so that if you click on another one, the previous one stays selected as well as the new one.
Any help is much appreciated.
For clarity. I understood
Another feature which I would like, if possible, would be that
clicking on a row would toggle it's selected property so that if you
click on another one, the previous one stays selected as well as the
new one.
in the way, that you want the CheckBox of the an item, respectively the Selected property on the items ViewModel, to stay selected, when the next DataGridRow is selected, but not the DataGridRow itself? Is that correct?
My suggestion is to extend the behavior of your DataGrid using *WPF behavior*s (This is a good introduction. This way you can keep your codebehind clear, but don't have to twist XAML to make it do what you want.
This is basically the idea of behaviors: Writing testable code, which is not coupled to your concrete view, but nonetheless allowing you to write complicated stuff in 'real' code and not in XAML. In my opinion your case is a typical task for behaviors.
Your behavior could look about as simple as this.
public class CustomSelectionBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
// Set mode to single to be able to handle the cklicked item alone
AssociatedObject.SelectionMode = DataGridSelectionMode.Single;
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
// Get DataContext of selected row
var item = args.AddedItems.OfType<ItemViewModel>();
// Toggle Selected property
item.Selected = !item.Selected;
}
}
Attaching the behavior to your specific DataGrid, is done in XAML:
<DataGrid ...>
<i:Interaction.Behaviors>
<b:CustomSelectionBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
You need to reference
System.Windows.Interactivity.dll
which contains the Behavior<T> baseclass as well.

How do i handle cell double click event on WPF DataGrid, equivalent to windows DataGrid's Events?

As you know, in windows C#'s gridview, if we want to handle a click/double click event on cell then there are events like CellClick, CellDoubleClick, etc.
So, i wanna do same like as windows gridview with WPF DataGrid. I have searched so far but neither answer is applicable nor useful. Some of them says use the MouseDoubleClick event but, in this event, we have to check for each row as well as item in that row, so it is time consuming to check every cell for data and timing is most important here.
My DataGrid is bounded to DataTable and AutoGeneratedColumn is False. If your answer is based on AutoGeneratedColumn=True then it is not possible. Even, i 'm changing the styles of datagrid cell according to data, so there is no way to change AutoGeneratedColumn property.
A Cell Clicking/Double Clicking event should be as faster as windows grid's event. If it is possible then tell me how, and if not, then what is the alternative to do it?
Please Help Me.....
Thanks a lot....
I know this may be a little late to the party, but this might be useful to someone else down the road.
In your MyView.xaml:
<DataGrid x:Name="MyDataGrid" ...>
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="MouseDoubleClick" Handler="DataGridCell_MouseDoubleClick"/>
</Style>
</DataGrid.Resources>
<!-- TODO: The rest of your DataGrid -->
</DataGrid>
In your MyView.xaml.cs:
private void DataGridCell_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var dataGridCellTarget = (DataGridCell)sender;
// TODO: Your logic here
}
An alternative way would to be define a DataGridTemplateColumn instead of using the predefined columns like DataGridCheckBoxColumn, DataGridComboBoxColumn and then add an event handler to the UI element defined in the data template.
Below I have defined a MouseDown event handler for a TextBlock Cell.
<DataGrid AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock MouseDown="TextBlock_MouseDown"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
In the Code behind file:
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
TextBlock block = sender as TextBlock;
if (block != null)
{
// Some Logic
// block.Text
}
}
I know coding WPF is sometimes a PITA. Here you would have to handle the MouseDoubleClick event anyway. Then search the source object hierarchy to find a DataGridRow and do whatever with it.
UPDATE: Sample code
XAML
<dg:DataGrid MouseDoubleClick="OnDoubleClick" />
Code behind
private void OnDoubleClick(object sender, MouseButtonEventArgs e)
{
DependencyObject source = (DependencyObject) e.OriginalSource;
var row = GetDataGridRowObject(source);
if (row == null)
{
return;
}
else
{
// Do whatever with it
}
e.Handled = true;
}
private DataGridRow GetDataGridRowObject(DependencyObject source)
{
// Write your own code to recursively traverse up via the source
// until you find a DataGridRow object. Otherwise return null.
}
}
I have used something like this:
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowOverlay}" CommandParameter="{Binding Parameter}" />
</DataGrid.InputBindings>
And handle my commands in my View Model.

Silverlight KeyDown event in behavior

In my Silverlight 4 DataGrid control, I wanted to attach a very simple Behavior which executes a custom command on key Press - actually, commit the selected item in the DataGrid on ENTER key press.
While the Behavior actually works (see my code...
//.... in "OnAttached()..."
this.AssociatedObject.AddHandler(Control.KeyDownEvent, new KeyEventHandler(OnKeyDown), true);
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
InvokeCommand();
}
}
...) I have the problem, that the DataGrid seems to handle the ENTER key press itself and proceeds to the next row. Obviously, what happens is that the wrong Row is committed, because when I handle the Key Press, the row selectedion has already changed.
Here is the XAML:
<data:DataGrid
AutoGenerateColumns="False"
IsReadOnly="True"
ItemsSource="{Binding Path=Data}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
<data:DataGrid.Columns>
<data:DataGridTextColumn Binding="{Binding A}" />
<data:DataGridTextColumn Binding="{Binding B}" />
<data:DataGridTextColumn Binding="{Binding C}" />
</data:DataGrid.Columns>
<i:Interaction.Behaviors>
<behaviors:EnterBehavior Command="{Binding CommitCommand}" />
</i:Interaction.Behaviors>
</data:DataGrid>
Can you tell me how I can prevent the default ENTER event?
Guess it's a bit late now to help the OP, but I solved this by subclassing the data grid and overriding the KeyDown method to set e.Handled to true. That stops the default enter processing of the DataGrid, then your own actions can take effect.
(Obviously you have to replace instances of DataGrid in the XAML with YourCustomDataGrid)
public class YourCustomDataGrid : DataGrid
{
protected override void OnKeyDown(KeyEventArgs e)
{
// Stop "Enter" selecting the next row in the grid
if (e.Key == Key.Enter)
{
e.Handled = true;
}
base.OnKeyDown(e);
}
}
Don't rely on SelectedRow, use the row that raised the event in the first place as a parameter for your submit operation. See code below:
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
InvokeCommand(e.OriginalSource);
}
}
See if using the AddHandler overload with handledEventsToo can help you here. In certain cases, this allows you to get your handler invoked even if a previous handler already set handled=true.

Resources