Getting ListBoxItem from item template control event - c#

In the WPF XAML code below, if I am in the SelectTaskItemClick event for the templated Button, how do I get the ListBoxItem ItemSource object that is currently selected?
<!-- ListBox ITEMS -->
<TaskDash:ListBoxWithAddRemove x:Name="listBoxItems" Grid.Row="1" Grid.Column="3" Grid.RowSpan="3"
ItemsSource="{Binding}">
<!--ItemsSource="{Binding}" DisplayMemberPath="Description">-->
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=Selected}"/>
</Style>
<TaskDash:ListBoxWithAddRemove.ItemTemplate>
<DataTemplate>
<DockPanel>
<Button DockPanel.Dock="Left" Click="SelectTaskItemClick">SELECT</Button>
<TextBox DockPanel.Dock="Left" Name="EditableDescription" Text="{Binding Description}" Height="25" Width="100" />
<Button DockPanel.Dock="Left" Click="EditTaskItemClick">EDIT</Button>
</DockPanel>
</DataTemplate>
</TaskDash:ListBoxWithAddRemove.ItemTemplate>
</TaskDash:ListBoxWithAddRemove>
If I try to get the Parent or TemplateParent, it gives me the ContentPresenter or Style or something similar.
private void SelectTaskItemClick(object sender, RoutedEventArgs e)
{
Button taskItemButton = (Button) e.OriginalSource;
ContentPresenter taskItem = (ContentPresenter) taskItemButton.TemplatedParent;
taskItem = (ContentPresenter)taskItemButton.TemplatedParent;
Style taskItem2 = taskItem.TemplatedParent;
taskItem2 = taskItem.TemplatedParent;
DependencyObject taskItem3 = taskItem2.Parent;
//DependencyObject taskItem3 = taskItem2.TemplatedParent;
//TaskItem taskItemObj = taskItem2;
}
In the code above, I'm guessing it is grabbing that from App.XAML where that custom ListBoxWithAddRemove control is defined. How do I traverse the actual form's XAML instead [the first code shown above]?
<Style x:Key="{x:Type TaskDash:ListBoxWithAddRemove}" TargetType="{x:Type TaskDash:ListBoxWithAddRemove}">
<Setter Property="Margin" Value="3" />
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TaskDash:ListBoxWithAddRemove}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Grid.Row="0"
Click="DeleteControlClick">Delete</Button>
<Button Grid.Column="1" Grid.Row="0"
Click="AddControlClick">Add</Button>
<Border
Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
Name="Border"
Background="{StaticResource WindowBackgroundBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1"
CornerRadius="2">
<ScrollViewer
Margin="0"
Focusable="false">
<StackPanel Margin="0" IsItemsHost="True" />
</ScrollViewer>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

You can use the VisualTreeHelper to walk up the tree and stop if you have an object of the right type, e.g.
private void SelectTaskItemClick(object sender, RoutedEventArgs e)
{
var b = sender as Button;
DependencyObject item = b;
while (item is ListBoxItem == false)
{
item = VisualTreeHelper.GetParent(item);
}
var lbi = (ListBoxItem)item;
//...
}
(If you just want to select the item that can (and should) just be done via the established binding, e.g.)
private void SelectTaskItemClick(object sender, RoutedEventArgs e)
{
// The DataContext should be an item of your class that should
// have a Selected property as you bind to it in a style.
var data = (sender as FrameworkElement).DataContext as MyClass;
data.Selected = true;
}

Assuming that
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=Selected}"/>
</Style>
works the way that it appears you intended, you should be able to loop through the items in your DataContext used as the ItemsSource for your listbox and check the Selected property of each to find the one currently selected. The more typical way of determining the selected item from a ListBox is by using listBox.SelectedItem where listBox is a variable that refers to the ListBox in question. Alternatively you might be able to access it off the sender parameter to the SelectTaskItemClick method. Another method you might try is helper methods to traverse the visual tree such as described at The Coding Bloke, and The Code Project - LINQ to Visual Tree.

Related

Launch MouseEnter event Image from XAML dictionnary with MVVM

I have a template for a DataGrid located in a ResourceDictionary.
BlockStyles.xaml
<Style TargetType="DataGrid" x:Key="SearchExpGrid">
<Setter Property="AlternatingRowBackground" Value="#4C87C6ff"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="CellStyle">
<Setter.Value ... />
</Setter>
<Setter Property="RowStyle">
<Setter.Value ... />
</Setter>
<Setter Property="ColumnHeaderStyle">
<Setter.Value>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border
x:Name="Border" Background="White" BorderBrush="#4C87C6" BorderThickness="1" >
<StackPanel Orientation="Horizontal" Margin="5,5" HorizontalAlignment="Center">
<TextBlock
x:Name="TxtB" Text="{Binding}"
Foreground="#4C87C6" FontWeight="DemiBold" HorizontalAlignment="Center"/>
<Image Source="../Images/dropdown.png"
Width="10" Height="10" Margin="5,0,5,0"
MouseEnter="DropdownButton_MouseEnter"
MouseLeave="DropdownButton_MouseLeave"
MouseEnter="DropdownButton_Click"/>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#4C87C6"/>
<Setter TargetName="TxtB" Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
I always have an error because the function can't find it's definition.
I first implemented the function in the View file where the style is used but it doesn't work.
I tried this method from StackOverflow using a resource class inheriting ResourceDictionnary but got the same error.
I then tried to use ICommand and RelayCommand to execute the function from the ViewModel but didn't got any result.
I also didn't find where I could add an EventHandler ImgDropdownButton.MouseEnter += new MouseEventHandler(MouseEnter_DropdownButton); using MVVM.
Is there a better solution for this kind of behaviour or if adding an EventHandler is the best solution, where sould be the best place to add it ?
Thanks in advance
Edit :
I managed to handle the function using a code-behind file for my ResourceDictionary following this.
BlockStyles.xaml.cs
private void DropdownButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
Mouse.OverrideCursor = Cursors.Hand;
}
private void DropdownButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
Mouse.OverrideCursor = Cursors.Arrow;
}
private void DropdownButton_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
//Function to show the popup
}
The MouseEnter and MouseLeave function are working, but I don't understand how to use the function to make my popup appear.
What I'm trying to do is that when I click on the Dropdown Image on the column header, I want to display a Popup, like an Excel one. This will allow the user to filter the columns values.
The file where my Grid and Popup are : (SearchExpView.xaml)
<Grid Grid.Row="2" Grid.Column="1">
<searchcomponents:ExpListView x:Name="ExpDatagrid"
DataContext="{Binding OExpListVM}"
Width="auto" Height="auto"/>
</Grid>
<Popup x:Name="PopupFiltre">
PopupFiltre content
</Popup>
Definition of my Datagrid : (ExpListView.xaml)
<Grid>
<DataGrid x:Name="ExpGrid" Style="{StaticResource SearchExpGrid}"
BorderThickness="0" BorderBrush="#4C87C6"
HorizontalAlignment="Left" VerticalAlignment="Top"
MinHeight="200" Height="auto" Margin="10,10,0,0"
MinWidth="780" Width="auto"
ItemsSource="{Binding}" DataContext="{Binding tableExpertise.DefaultView}"
AutoGenerateColumns="True" CanUserAddRows="False" IsReadOnly="True">
<DataGrid.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick"
Command="{Binding DataContext.OnRowDoubleClickedCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding ElementName=ExpGrid, Path=CurrentItem}"/>
</DataGrid.InputBindings>
<DataGrid.ContextMenu>
<ContextMenu Name="dgctxmenu">
<Separator></Separator>
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</Grid>
I'm looking for a way to be able to implement this popup fonction but I can't find out how to link everything together.
My Window is SearchExpView.xaml (with the Datagrid and the Popup). My Datagrid component is defined in ExpListView.xaml and styled in BlockStyles.xaml, which is not a window. I want to make the Popup (in SearchExpView.xaml) visible by clicking on the dropdown button (defined in BlockStyles.xaml)
Then you need to get a reference to the Popup in the window from the ResourceDictionary somehow.
You could for example use the static Application.Current.Windows property:
var window = Application.Current.Windows.OfType<SearchExpView>().FirstOrDefault();
if (window != null)
window.PopupFiltre.IsOpen = true;
Also make sure that you make the Popup accessible from outside the SearchExpView class:
<Popup x:Name="PopupFiltre" x:FieldModifier="internal">
...

WPF Custom control reset textbox text to a default value

I need a text box with a button in it,it must display a default value but should still allow the user to type into a text that i need to store in my ViewModel property.
The button should reset the value to the default one.
I got few issues with this implementation:
When the user type into the textbox i would expect the bound property in my viewModel to update accordingly, but seems there is no binding anymore. (Binding is set two way)
(the binding and the DataContext is correct, as on load is displaying the value set from the ViewModel)
Once i type into the box and hit the revert button the text is assign to the property as expected, but the text box still display he same value type by the user.
Each time i move across tabs o click another control, the button responsible for revert the text back, needs to be clicked twice (looks like a focus issue) as once the focus is in the text box all is working normally.
I have created a Generic.xaml were i have defined the control template.
<Style x:Key="{x:Type local:RememberValue}" TargetType="{x:Type local:RememberValue}">
<Setter Property="Background" Value="{StaticResource RemeberValue_Background}" />
<Setter Property="BorderBrush" Value="{StaticResource RemeberValue_Border}" />
<Setter Property="Foreground" Value="{StaticResource RemeberValue_Foreground}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Focusable" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RememberValue}">
<Grid x:Name="LayoutGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<baseControlUi:IconButton
Grid.Column="0"
Height="22"
Grid.ZIndex="1"
Margin="0"
EllipseDiameter="19"
Focusable="True"
Visibility="{Binding ElementName=RememberValueControl, Path=IsDifferentValue, Converter={StaticResource BooleanToVisibilityConverter}}"
ButtonCommand="{TemplateBinding RevertCommand}"
ButtonIcon="{StaticResource RevertIcon}" />
<TextBox
Grid.ZIndex="0"
Foreground="{StaticResource RemeberValue_Foreground}"
Text="{TemplateBinding DisplayText}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This is the usage in the View.
<StackPanel Width="400">
<remebervalue:RememberValue
DisplayText="{Binding DisplayText, UpdateSourceTrigger=PropertyChanged}"
DefaultValue="{Binding DefaultText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" Width="400" />
</StackPanel>
the code behind of RemeberValue.cs ha DP registered for the DisplayText and the DefaultText
public static readonly DependencyProperty DisplayTextProperty =
DependencyProperty.Register(nameof(DisplayText), typeof(string), typeof(RememberValue), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnDisplayText_Changed));
public RememberValue()
{
RevertCommand = new SimpleCommand(Revert);
}
private void Revert()
{
DisplayText = DefaultValue;
}
public string DisplayText
{
get => (string)GetValue(DisplayTextProperty);
set => SetValue(DisplayTextProperty, value);
}
private static void OnDisplayText_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RememberValue RememberValue = d as RememberValue;
}
Partial answer
First point: I believe you are mistaken in writing "Binding is set two way", as you are using TemplateBinding, which is always one-way. You should replace it with
Binding DisplayText, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay
Second point: fixed by the above
Third point: different issue, needs to be addressed in a different question.

How to persistently change color of ListBox SelectedItem after selecting

I have a listbox that loads it's items with Foreground color set to red. What I'd like to do is: upon selecting an item with the mouse, change the foreground color of SelectedItem to black, but make the change persistent so that after deselecting the item, color remains black. Incidentally I want to implement this as a way of showing 'read items' to the user.
Essentially I want something like an implementation of the common property trigger like the code below, but not have the style revert after deselection. I've played around with event triggers as well without much luck.
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="Foreground" Value="Black" /> //make this persist after deselection
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Thanks in advance!
You could animate the Foreground property:
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(ListBoxItem.Foreground).(SolidColorBrush.Color)"
To="Black" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The downside of this simple approach is that the information is not stored somewhere. This is pure visualization without any data backing. In order to persist the information, so that restarting the application shows the same previous state, you should introduce a dedicated property to your data model e.g IsMarkedAsRead.
Depending on your requirements, you can override the ListBoxItem.Template and bind ToggleButton.IsChecked to IsMarkedAsRead or use a Button which uses a ICommand to set the IsMarkedAsRead property. There are many solutions e.g. implementing an Attached Behavior.
The following examples overrides the ListBoxItem.Template to turn the ListBoxItem into a Button. Now when the item is clicked the IsMarkedAsRead property of the data model is set to true:
Data model
(See Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern for an implementation example of the RelayCommand.)
public class Notification : INotifyPropertyChanged
{
public string Text { get; set; }
public ICommand MarkAsReadCommand => new RelayCommand(() => this.IsMarkedAsRead = true);
public ICommand MarkAsUnreadCommand => new RelayCommand(() => this.IsMarkedAsRead = false);
private bool isMarkedAsRead;
public bool IsMarkedAsRead
{
get => this.isMarkedAsRead;
set
{
this.isMarkedAsRead = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
ListBox
<ListBox ItemsSource="{Binding Notifications}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="{TemplateBinding Background}">
<Button x:Name="ContentPresenter"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}, Path=ItemTemplate}"
Content="{TemplateBinding Content}"
Command="{Binding MarkAsReadCommand}"
Foreground="Red">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsMarkedAsRead}" Value="True">
<Setter TargetName="ContentPresenter" Property="Foreground" Value="Green" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Notification}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Thanks a lot #BionicCode for the comprehensive answer. I ended up going with another solution which may or may not be good convention; I am a hobbyist.
Firstly, I don't need databacking / persistence.
Concerning the data model solution and overriding ListBoxItem.Template, I am using a prededfined class 'SyndicationItem' as the data class (my app is Rss Reader). To implement your datamodel solution I guess I could hack an unused SyndicationItem property, or use SyndicationItem inheritance for a custom class (I'm guessing this is the most professional way?)
My complete data model is as follows:
ObservableCollection >>> CollectionViewSource >>> ListBox.
Anyway I ended up using some simple code behind which wasn't so simple at the time:
First the XAML:
<Window.Resources>
<CollectionViewSource x:Key="fooCollectionViewSource" Source="{Binding fooObservableCollection}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="PublishDate" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Style x:Key="DeselectedTemplate" TargetType="{x:Type ListBoxItem}">
<Setter Property="Foreground" Value="Gray" />
</Style>
</Window.Resources>
<ListBox x:Name="LB1" ItemsSource="{Binding Source={StaticResource fooCollectionViewSource}}" HorizontalContentAlignment="Stretch" Margin="0,0,0,121" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock MouseDown="TextBlock_MouseDown" Grid.Column="0" Text="{Binding Path=Title.Text}" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextAlignment="Center" FontSize="11" FontWeight="SemiBold"
Text="{Binding Path=PublishDate.LocalDateTime, StringFormat='{}{0:d MMM, HH:mm}'}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now the code behind:
Solution 1: this applies a new style when listboxitem is deselected. Not used anymore so the LB1_SelectionChanged event is not present in the XAML.
private void LB1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count != 0)
{
foreach (var lbItem in e.RemovedItems)
{
//get reference to source listbox item. This was a pain.
int intDeselectedItem = LB1.Items.IndexOf(lbItem);
ListBoxItem lbi = (ListBoxItem)LB1.ItemContainerGenerator.ContainerFromIndex(intDeselectedItem);
/*apply style. Initially, instead of applying a style, I used mylistboxitem.Foreground = Brushes.Gray to set the text color.
Howver I noticed that if I scrolled the ListBox to the bottom, the text color would revert to the XAML default style in my XAML.
I assume this is because of refreshes / redraws (whichever the correct term). Applying a new style resolved.*/
Style style = this.FindResource("DeselectedTemplate") as Style;
lbi.Style = style;
}
}
}
Solution 2: The one I went with. Occurs on SelectedItem = true, same effect as your first suggestion.
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
TextBlock tb = e.Source as TextBlock;
tb.Foreground = Brushes.Gray;
}

WPF ListBoxItem ControlTemplate breaks some MouseDown/Selection

I have an issue with ListBoxItems. I am trying to make all controls in the ListBoxItem select it as well, so clicking on a TextBox, Label, etc will select the ListBoxItem. Pretty simple so far.
I am also changing the ListBoxItem Template to change the selection visualization from highlighting the background to just drawing a border. Also pretty simple.
The combination of these two, however, seems to cause some really irritating issues with MouseDown and PreviewMouseDown, specifically in my case regarding Labels in a Grid, where one creates a "void" occupied by Grid space.
Using snoop, I can see the PreviewMouseDown event stopping at the ScrollViewer inside the ListBox, and not going all the way to the ListBoxItem.
XAML:
<Window x:Class="ListBoxClickThroughTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="350">
<Grid>
<ListBox ItemsSource="{Binding Items}"
SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Name="VerySuperLongLabel"
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Left"
Content="VerySuperLongLabel"
Padding="0" />
<TextBox Name="Textbox1"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Right"
Text="Textbox1 Text" />
<Label Name="ShortLabel"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Left"
Content="ShortLabel"
Padding="0" />
<TextBox Name="Textbox2"
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Right"
Text="Textbox2 Text" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="PreviewMouseDown"
Handler="ListBoxItem_PreviewMouseDown" />
<EventSetter Event="MouseDown"
Handler="ListBoxItem_PreviewMouseDown" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="Bd"
BorderThickness="1">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="Bd" Property="BorderBrush" Value="Gray" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Window>
Code-behind:
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace ListBoxClickThroughTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
Items = new List<string>() { "1", "2" };
InitializeComponent();
DataContext = this;
}
public List<string> Items { get; set; }
private void ListBoxItem_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var listBoxItem = (ListBoxItem)sender;
listBoxItem.IsSelected = true;
}
}
}
However, if I remove the Template setter, all is well. Is there some magic in the template I'm missing? I tried renaming the border to "Bd" as that was what the default template border was named, but no luck. Any ideas?
If you change the horizontal alignment of the labels from "Left" to "Stretch" this will fix the problem and keep the visual formatting the same.
Mousedown events only work in areas where elements exist. By having the labels at a "left" horizontal alignment, you are creating the "void" you mentioned, where no element exists at that level that can be clicked. To visually see the difference, try temporarily setting the background property of the label elements that are giving you problems, and you'll see the element doesn't extend all the way to the textbox.

WPF - MenuItem missing Icon/Image

Im getting menuItem icon appearing only on last menuItem.
If i snoop the app only last menuItem has image in icon, while if i debug all MenuItems appear to have image in icon. Also if i add submenuItem the icon on menuItem dissapears once i open submenus and the last submenu gets the icon... Any idea? PS: also tooltips on menu item dont work.
Im using caliburn micro and fluent ribbon controls.
<ControlTemplate x:Key="dropDownButton">
<ef:DropDownButton Header="{Binding DisplayName}"
ItemsSource="{Binding Items}"
LargeIcon="{Binding LargeIconPath}"
cm:Message.Attach="ClickAction()"
ef:KeyTip.Keys="{Binding KeyTip}">
<ef:DropDownButton.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header"
Value="{Binding DisplayName}"/>
<Setter Property="Icon">
<Setter.Value>
<Image Source="{Binding Path=IconPath}"/>
</Setter.Value>
</Setter>
<Setter Property="ItemsSource"
Value="{Binding Items}"/>
<Setter Property="cm:Message.Attach"
Value="ClickAction()"/>
<Setter Property="ef:KeyTip.Keys"
Value="{Binding KeyTip}"/>
<Setter Property="ToolTip">
<Setter.Value>
<ef:ScreenTip Title="{Binding DisplayName}"
HelpTopic="ScreenTip help ..."
Image="{Binding LargeIconPath}"
Text="Text for ScreenTip"/>
</Setter.Value>
</Setter>
</Style>
</ef:DropDownButton.ItemContainerStyle>
<ef:DropDownButton.ToolTip>
<ef:ScreenTip Title="{Binding DisplayName}"
HelpTopic="ScreenTip help ..."
Image="{Binding LargeIconPath}"
Text="Text for ScreenTip"/>
</ef:DropDownButton.ToolTip>
</ef:DropDownButton>
You are setting Icon property to an Image control in Style. Now, only one copy of Style is created and thus, only one copy of Image is created. Now, any control can have only one parent at a time. So, when it is assigned to last MenuItem, it is removed from previous MenuItem controls. To fix this, use Templates.
Instead of setting Header property, set HeaderTemplate:
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Source="{Binding Path=IconPath}" />
<TextBlock Grid.Column="1"
Text="{Binding DisplayName}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
I'm not sure of what properties are exposed by the control toolkit you are using. But, I'm sure they must have a template property.
After doing this, you don't need to set Icon property in style.
I successfully use the following entries in a ResourceDictionary:
<!-- Define non-shared image to avoid loss of menu icons -->
<Image x:Key="MenuIconImage" Height="16" Width="16" x:Shared="false">
<Image.Source>
<DrawingImage Drawing="{Binding Icon}" />
</Image.Source>
</Image>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="Header" Value="{Binding DisplayName />
<Setter Property="Icon" Value="{StaticResource MenuIconImage}" />
</Style>
Works like this:
<DataTemplate x:Key="MenuItemHeaderTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{Binding Path=IconPath}" />
<Label Grid.Column="1" Content="{Binding DisplayName}" />
</Grid>
</DataTemplate>
<ControlTemplate x:Key="dropDownButton">
<ef:DropDownButton Header="{Binding DisplayName}"
ItemsSource="{Binding Items}"
LargeIcon="{Binding LargeIconPath}"
cm:Message.Attach="ClickAction()"
ef:KeyTip.Keys="{Binding KeyTip}">
<ef:DropDownButton.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="HeaderTemplate" Value="{StaticResource MenuItemHeaderTemplate}" />
<Setter Property="ItemsSource"
Value="{Binding Items}"/>
<Setter Property="cm:Message.Attach"
Value="ClickAction()"/>
<Setter Property="ef:KeyTip.Keys"
Value="{Binding KeyTip}"/>
<Setter Property="ToolTip">
<Setter.Value>
<ef:ScreenTip Title="{Binding DisplayName}"
HelpTopic="ScreenTip help ..."
Image="{Binding LargeIconPath}"
Text="Text for ScreenTip"/>
</Setter.Value>
</Setter>
</Style>
</ef:DropDownButton.ItemContainerStyle>
<ef:DropDownButton.ToolTip>
<ef:ScreenTip Title="{Binding DisplayName}"
HelpTopic="ScreenTip help ..."
Image="{Binding LargeIconPath}"
Text="Text for ScreenTip"/>
</ef:DropDownButton.ToolTip>
</ef:DropDownButton>
For some reason approach when Image is static resource with x:Shared = false doesn't work for me. Only last menu item shows icon. I've tried both StaticResource and DynamicResource. Here is my solution:
public class MenuItemIconHelper
{
#region ImageSource Icon
public static readonly DependencyProperty IconProperty = DependencyProperty.RegisterAttached("Icon", typeof(ImageSource), typeof(MenuItemIconHelper), new PropertyMetadata(default(ImageSource), IconPropertyChangedCallback));
private static void IconPropertyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var i = (MenuItem)obj;
if (e.NewValue != null)
i.Icon = new Image() {Source = (ImageSource)e.NewValue};
else
i.Icon = null;
}
public static void SetIcon(DependencyObject element, ImageSource value)
{
element.SetValue(IconProperty, value);
}
public static ImageSource GetIcon(DependencyObject element)
{
return (ImageSource)element.GetValue(IconProperty);
}
#endregion
}
Sample:
<Style x:Key="CommandMenuItemStyle" TargetType="MenuItem">
<Setter Property="cb:MenuItemIconHelper.Icon" Value="car1.png" />
<Setter Property="Header" Value="{Binding Name}" />
I consider it to be more readable than using resource and you don't need to change MenuItem's HeaderTemplate. You can also implement some caching mechanism for ImageSource or Image.
1. Add Existing File... image file to resources (if you already have one, skip it).
2. In Solution Explorer select this image file.
3. Change Build Action to Resource.
And finally, you can add this image to XAML with simple call:
<Window.Resources>
<ContextMenu x:Key="contextMenu" >
<MenuItem Header="Restart" Name="menuItemRestart" Click="MenuItem_Click">
<MenuItem.Icon>
<Image Source="/Resources/restart.png"/>
</MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="Exit" Name="menuItemExit" Click="MenuItem_Click">
<MenuItem.Icon>
<Image Source="/Resources/window_close.png"/>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Window.Resources>
The Result:

Categories