Trapping events within list box item templates in WPF - 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...
}

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.

<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; }
}

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 how to promote selected item of listbox when using buttons within datatemplates

Within a Listbox control I have a Data Template which consists of text and a button. Given the nature of Silverlight/WPF when I click on the button within the listbox item the button event is trapped before the listbox item is selected. Therefore if I am trying to pass the record ID of the selected listbox item I am currently only able to do so by first clicking and selecting the listbox item and then clicking on the button.
Is there a way to promote the selection of the listbox item so that when the listbox items are created I have the ability to click on the button within the listbox item and some event (selectionChanged ?) is invoked which would allow me to capture the selected record id and use it for some other action ( pass as a parameter in a method etc). I'm using Simple MVVM toolkit for this implementation so I was wondering if this could be handled in the viewModel or if I would need to handle this in the controls code behind and then push the selection to the viewModel.
The listbox control is presented as:
<ListBox x:Name="ResultListBox"
HorizontalAlignment="Stretch"
Background="{x:Null}"
Grid.Row="1"
BorderThickness="0" HorizontalContentAlignment="Stretch"
ItemContainerStyle="{StaticResource ListBoxItemStyle1}"
ItemsSource="{Binding SearchResults[0].Results}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
Style="{StaticResource ListBoxStyle1}">
<ListBox.ItemTemplate>
<DataTemplate>
<dts:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch">
<!-- Template 1 -->
<formatter:TypeTemplateSelector.CFSTemplate>
<DataTemplate>
<qr:ucIndex_Product />
</DataTemplate>
</formatter:TypeTemplateSelector.CFSTemplate>
<!-- Template 2 -->
<formatter:TypeTemplateSelector.PersonTemplate>
<DataTemplate>
<qr:ucIndex_Person />
</DataTemplate>
</formatter:TypeTemplateSelector.PersonTemplate>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Within the datatemplate (user control) resides the button along with a number of other fields. I'll omit that code for the time being unless requested.
Thanks in advance!
Put this in your ListBox.Resources
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="PreviewGotKeyboardFocus" Handler="SelectCurrentItem"/>
</Style>
And this in the Code Behind
protected void SelectCurrentItem(object sender, KeyboardFocusChangedEventArgs e)
{
ListBoxItem item = (ListBoxItem)sender;
item.IsSelected = true;
}
You could use the following code as well which doesn't use code-behind, however it only keeps the ListBoxItem selected for as long as it has KeyBoard focus. Once focus leaves, the item becomes unselected
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
EDIT
Since Silverlight doesn't have EventSetters, you can use the ListBox's Loaded event and add the following to your code behind:
private void ResultListBox_Loaded(object sender, RoutedEventArgs e)
{
ListBox list = (ListBox)sender;
list.GotFocus += ResultListBox_GotFocus;
}
void ResultListBox_GotFocus(object sender, RoutedEventArgs e)
{
var item = FindAncester<ListBoxItem>((DependencyObject)e.OriginalSource);
if (item != null) item.IsSelected = true;
}
T FindAncester<T>(DependencyObject current)
where T : DependencyObject
{
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
This captures the Focus event for the ListBox, takes the control that triggered the focus event and traverses up the visual tree to find the ListBoxItem objects, and sets it's Selected value to true.
Rachel's solution works great. The one issue I did find in this approach was that it does place total focus on the selected item. As a result the user would be required to double click within the control to place focus on other items such as selectable text or other button. After working with this a bit more I discovered you can also resolve this by setting the listbox selected items to the data context of the object you are clicking on etc. This works well as allows you to set this to any UI object within the control.
ListBox.SelectedItem = ((HyperlinkButton)sender).DataContext;
In this example I had Hyperlink buttons within the data template. Clicking on them would then set the focus to the selected listbox item.
rlcrews got it right! Use DataContext:
ObservableCollection<Employee> employees1;
...
listBox1.ItemsSource = employees1;
...
//DataTemplate in ListBox has a button with following event
private void bnPromoteEmployee_Click(object sender, RoutedEventArgs e)
{
Employee emp1 = (Employee)((Button)sender).DataContext;
emp1.Promote();
}

How to access ItemsControl's ItemsSource element from the ItemsControl's Item's code behind?

In my main view I have an ItemsControl which is bound to a collection of objects:
<ItemsControl ItemsSource="{Binding Path=Concepts}"
ItemTemplate="{StaticResource ActivationLevelTemplate}"
/>
Where the ActivationLevelTemplate is just another view:
<DataTemplate x:Key="ActivationLevelTemplate">
<view:ConceptActivationView Height="50"/>
</DataTemplate>
In this view there is a text block, bound to a property of an object from the collection mentioned above. The property is displayed correctly, and now I need to access other properties of the same object from the view's code behind. It seems trivial but I could not get it working.
<TextBlock Text="{Binding Path=Name}"
HorizontalAlignment="Center"
/>
<d3:Plotter2D Name="Plotter"/>
The best thing I came across was ItemContainerGenerator but it does not seem to be what is needed.
What is important is the context in which you try to access that object. If you for example deal with an event inside the DataTemplate you can easily get the object from the DataContext of the sender (has to be a FrameworkElement), e.g. if i were to handle a button click:
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = (FrameworkElement)sender;
var employee = (Employee)button.DataContext;
//...
}
In fact if your whole view is inside the DataTemplate you can get the object directly from the View's DataContext as well.
You should be able to iterate through the items in the ItemsControl and get all the properties you need. Give the ItemsControl a name so you can address it in the code behind:
<ItemsControl Name="itemsControl" ... />
Then in code behind
foreach (YourItem item in itemsControl.Items)
{
// your logic...
}
If you need a specific item you could try CurrentItem or GetItemAt() instead
itemsControl.Items.CurrentItem
// or
itemsControl.Items.GetItemAt()

Resources