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

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.

Related

<Image> source not working on binding

I am trying to bind a listbox via ItemsTemplate to a collection of custom "Document" objects but am having an issue while trying to bind an image to the Document.ImageResourcePath property. Here is my markup
<ListBox Name="lbDocuments">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Path=ImageResourcePath}"
Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is my load event for the form that has the listbox.
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
List<Objects.Document> docs = Objects.Document.FetchDocuments();
lbDocuments.ItemsSource = docs;
}
My Document class holds a string to a resource image located in my resources folder depending on the document extension.
e.g. (this is part of a case statement within the document class)
case Cache.DocumentType.Pdf:
this.ImageResourcePath = "/JuvenileOrganizationEmail;component/Resources/pdf_icon.jpg";
break;
When the Window loads I get absolutely nothing in my listbox when it is bound to 23 perfectly well Document types. What could I be doing wrong?
Use an ObservableCollection instead of a List, and make the reference "class level" to your Window.
ObservableCollection<Objects.Document> _docs;
Make sure the DataContext is set in the Window's Ctor.
public Window()
{
_docs = new ObservableCollection<Objects.Document>(Objects.Document.FetchDocuments());
this.DataContext = this;
}
Then, you can just update your Window Loaded event:
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
lbDocuments.ItemsSource = _docs;
}
Or, an alternative solution, will be binding the ItemsSource of the ListBox directly to a public property of the collection. This is assuming the Ctor (above) is still used.
<ListBox Name="lbDocuments" ItemsSource={Binding Docs}>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Path=ImageResourcePath}" Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In your Window.cpp file (though, a separate ViewModel class may be recommended if you are doing MVVM)
public ObservableCollection<Objects.Document> Docs
{
get { return _docs; }
}

WPF Datagrid Items Refresh lose focus

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.

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.

How to bind EDM to WPF ListBox?

I'm trying to figure out WPF binding to SQLite.
I have an ADO.NET Entity Data Model generated to represent my SQLite database. The database only holds one table "People" with two columns "person_id" and "person_name". Now, I generated EDM classes for that table within my WPF Application.
I'm trying to bind to a list box. I can delete items from source and see it updating the list box. But I can't add items to the source using a text box and see it update the list box.
I declared data entities in Window1 class like this:
private static MyNewSqliteDbEntities2 _myEntities = new MyNewSqliteDbEntities2();
I have a list box that is bound to the ObjectQuery in the Window_Loaded event handler like this:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
peopleListBox.ItemsSource = _myEntities.People;
}
I have another text box which I use to add people by clicking on button. And I can delete Items by selecting an item in the list box and clicking delete button. Changes are committed to the database when a commit button is clicked. Please consider the code below:
private void addButton_Click(object sender, RoutedEventArgs e)
{
if (addPersonTextBox.Text != "")
{
People newPerson = new People();
newPerson.person_name = addPersonTextBox.Text;
//_myEntities.AddToPeople(newPerson);
_myEntities.AddObject("People", newPerson);
addPersonTextBox.Text = "";
}
}
private void deleteButton_Click(object sender, RoutedEventArgs e)
{
_myEntities.DeleteObject(peopleListBox.SelectedItem);
}
private void commitButton_Click(object sender, RoutedEventArgs e)
{
_myEntities.SaveChanges();
}
I tried refreshing the list box control using another button called "Refresh" in the following manner but with no luck (although, when I step through the code I see the source is updated):
private void refreshButton_Click(object sender, RoutedEventArgs e)
{
peopleListBox.ItemsSource = null;
peopleListBox.ItemsSource = _myEntities.People;
}
Here is the XAML code if you are wondering:
&ltWindow x:Class="BindingToSqLite.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="400" Width="400" Loaded="Window_Loaded"&gt
&ltWindow.Resources&gt
&ltDataTemplate x:Key="personNameTemplate"&gt
&ltTextBlock Text="{Binding Path=person_name}"/&gt
&lt/DataTemplate&gt
&lt/Window.Resources&gt
&ltGrid&gt
&ltGrid.ColumnDefinitions&gt
&ltColumnDefinition Width="190*" /&gt
&ltColumnDefinition Width="94*" /&gt
&ltColumnDefinition Width="94*" /&gt
&lt/Grid.ColumnDefinitions&gt
&ltGrid.RowDefinitions&gt
&ltRowDefinition Height="182*" /&gt
&ltRowDefinition Height="38*" /&gt
&ltRowDefinition Height="38*" /&gt
&ltRowDefinition Height="32*" /&gt
&lt/Grid.RowDefinitions&gt
&ltListBox Margin="5" Name="peopleListBox" Grid.ColumnSpan="3" ItemTemplate="{StaticResource personNameTemplate}" /&gt
&ltTextBox Grid.Row="1" Grid.ColumnSpan="2" Margin="5,10" Name="addPersonTextBox" /&gt
&ltButton Grid.Column="2" Grid.Row="1" Margin="5" Name="addButton" Click="addButton_Click"&gtAdd&lt/Button&gt
&ltButton Grid.Row="2" Margin="5" Name="commitButton" Click="commitButton_Click"&gtCommit&lt/Button&gt
&ltButton Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="2" Margin="5" Name="deleteButton" Click="deleteButton_Click"&gtDelete&lt/Button&gt
&ltButton Grid.Row="3" Margin="5" Name="refreshButton" Click="refreshButton_Click"&gtRefresh&lt/Button&gt
&lt/Grid&gt
&lt/Window&gt
I'm not sure if I'm doing this completely wrong. Any help is appreciated.
The People property needs to be an ObservableCollection<T> for this to work.

Trapping events within list box item templates in WPF

I've got listbox that employs an item template. Within each item in the list, as defined in the template there is a button. When the user clicks the button I change a value in the data source that defines the sort order of the list. Changing the datasource is not a problem as this is working just fine within my application template.
However my next step is to reload the listbox with the new sorted data source. I've tried doing this from the tempalte but it apparently doesn't have access (or I can't figure out how to get access) to the parent elements so I can reset the .ItemSource property with a newly sorted data source.
Seems like this is possible but the solution is eluding me :(
You could use databinding to bind the Button's Tag to its ListBox ancestor. Example:
<Grid>
<Grid.Resources>
<DataTemplate x:Key="myDataTemplate">
<Button Tag="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}}"
Click="Button_Click">MyButton</Button>
</DataTemplate>
</Grid.Resources>
<ListBox ItemTemplate="{StaticResource myDataTemplate}" ItemsSource="..." />
</Grid>
And here's the codebehind:
private void Button_Click(object sender, RoutedEventArgs e)
{
ListBox myListBox = (ListBox)((Button)sender).Tag;
...do something with myListBox...
}
Alternatively, you can manually climb the Visual Tree upwards in your code (no Tag data binding needed):
private void Button_Click(object sender, RoutedEventArgs e)
{
DependencyObject search = (DependencyObject)sender;
while (!(search is ListBox)) {
search = VisualTreeHelper.GetParent(search);
}
ListBox myListBox = (ListBox)search;
...do something with myListBox...
}

Resources