How can I get the SelectedItem of a ListBox to be highlighted after setting the SelectedItem in the ViewModel?
The ItemsSource is bound to an ObservableCollection of Bar (the collection is a member of a class Foo. A button is bound to a command that adds a new empty Bar instance to the collection and then also sets SelectedItem to the new empty instance.
After adding the instance to the collection, the ListBox is updated to show the new blank Bar. However, after setting the SelectedItem property in the ViewModel, the new instance is not highlighted in the ListBox but it is being set and the PropertyChanged event is raised (the SelectedItem is displayed elsewhere in the View).
Additional Details:
INotifyPropertyChanged is implemented in a base ViewModel class, and also implemented in the Foo and Bar classes.
The ListBox contains a custom ItemTemplate to display Bar members, and a custom ItemContainerStyle that modifies the Background for the IsMouseOver trigger.
simplified xaml:
<ListBox ItemsSource="{Binding Path=MyFoo.BarCollection}"
SelectedItem="{Binding Path=SelectedItem,
UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Button Content="Add New Bar"
Command="{Binding Path=AddBarCommand}"/>
simplified viewmodel:
private Foo _myFoo;
public Foo MyFoo
{
get { return _myFoo; }
set { _myFoo= value; OnPropertyChanged("MyFoo"); }
}
private Bar _selectedItem;
public Bar SelectedItem
{
get { return _selectedItem; }
set { _selectedItem = value; OnPropertyChanged("SelectedItem"); }
}
private void AddBar()
{
Bar newBar = new Bar();
MyFoo.BarCollection.Add(newBar);
SelectedItem = newBar ;
_unsavedChanges = true;
}
Your code worked perfectly for me.
Notice that "Bar 3" is selected, but when the listbox doesn't have focus, the default theme on Windows 7 works pretty hard to keep you from noticing. And Windows 7 isn't even the worst of the bunch. Our application uses WhiteSmoke as a default background, and we've had major issues with older users1 being unable to tell if listbox items are selected or not.
Fortunately, it's a trivial fix. These resources could just as easily be in Window.Resources, in a global ListBox style, or in App.xaml. It's up to you how widely you want to apply them.
<ListBox
ItemsSource="{Binding MyFoo.BarCollection}"
SelectedItem="{Binding SelectedItem}"
>
<ListBox.Resources>
<SolidColorBrush
x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}"
Color="{x:Static SystemColors.HighlightColor}"
Opacity="0.5"
/>
<SolidColorBrush
x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}"
Color="{x:Static SystemColors.HighlightTextColor}"
/>
</ListBox.Resources>
</ListBox>
1 "Older" meaning old enough to vote.
And if your version of .NET predates the existence of SystemColors.InactiveSelectionHighlightTextBrushKey, this could probably use some refinement, but it works:
<ListBox
>
<ListBox.ItemContainerStyle>
<Style
TargetType="ListBoxItem"
BasedOn="{StaticResource {x:Type ListBoxItem}}"
>
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid
Background="{TemplateBinding Background}"
>
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter
Property="Background"
Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"
/>
<Setter
Property="Foreground"
Value="{StaticResource {x:Static SystemColors.HighlightTextBrushKey}}"
/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True" />
<Condition Property="IsEnabled" Value="False" />
</MultiTrigger.Conditions>
<Setter
Property="Background"
Value="{StaticResource {x:Static SystemColors.InactiveCaptionBrushKey}}"
/>
</MultiTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Related
I have here a ListBox which shall show serveral items where some of them are selected.
<ListBox IsEnabled="False" x:Name="SecondaryModelSelector" SelectionMode="Extended" SelectedItem="{Binding Path=DataContext.SelectedSecondaryModels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}, Mode=OneWay}" ItemsSource="{Binding Path=DataContext.AvailableModels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}" Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The selected items are set in a ComboBox and thus the ListBox is disabled and Mode=OneWay to disable selection in the ListBox itself.
The Template looks like this:
<Style x:Key="MyListBoxItem" TargetType="ListBoxItem">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Border" Padding="2">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="Border" Property="Background" Value="Blue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MyListBoxBorder" TargetType="{x:Type ListBox}">
<Setter Property="ItemContainerStyle" Value="{StaticResource MyListBoxItem}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border x:Name="OuterBorder">
<ScrollViewer Margin="0" Focusable="false">
<StackPanel Margin="2" IsItemsHost="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The problem is now, that I cannot display multiple selected items, i.e. the background does not turn blue. Showing only one selected item is no problem.
Works (but shows only one selected item...)
public Cpu SelectedSecondaryModels
{
get { return SelectedGlobalVariable.SecondaryModels.FirstOrDefault(); }
}
Does not work:
public ObservableCollection<Model> SelectedSecondaryModels
{
get { return new ObservableCollection<Model>(SelectedGlobalVariable.SecondaryModels); }
}
I have no errors, so how can I show multiple selected items?
I tried SelectedItems instead of SelectedItem instead, but this is read-only field.
When I allow to select items in the ListBox than the blue background appears, and binding throws errors as there is no setter method.
The SelectedItem property cannot be bound to a collection of several items to be selected.
And as you have already noticed, SelectedItems is a read-only property.
You could use an attached behaviour to work around this. Please refer to this blog post and this example for more information about how to do this.
You will also find some solutions here:
Bind to SelectedItems from DataGrid or ListBox in MVVM
I think I'm close, but I'm missing something here.
I'm really trying to stretch my XAML knowledge and unclutter my code behind in my WPF projects.
I have a situation where I'd like to populate the items of a combobox based on the state of another control.
I have a style where I'm checking the value of a checkbox:
<Style
x:Key="{x:Type ComboBox}"
TargetType="{x:Type ComboBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding ElementName=SomeCheckBox, Path=IsChecked}"
Value="True">
Which seems to allow me to trigger on the value of the box.
I'm trying to figure out how to set up my setter now.
It seems that there is no way to access the items property ...
Here is what I've been messing with:
<Setter
Property="Items">
<Setter.Value>
<ComboBoxItem Content="SomeValue" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
but it's pretty clear that isn't going to work.
Is there a way to do this using the setter?
Do I need to come up with some sort of list or lists that I stow in the window resources and use as an item source?
And how would I make that list?
based on the state of another control
I would do it in the code behind on the VM after binding the check box IsChecked to a property while also binding the ComboBox's to a list which changes based on the value of IsChecked.
Here is a quick example:
public bool IsOperationChecked // Bound to the checkbox
{
get { return _IsOperationChecked; }
set {
_IsOperationChecked= value;
OnPropertyChanged("IsOperationChecked");
Names = (value) ? new List<string>() {"alpha", "beta"} :
new List<string>() {"Bill", "Frank"};
}
}
public List<string> Names // Bound to the Combobox
{
get { return _Names; }
set { _Names = value; OnPropertyChanged("Names"); }
}
Use an array declared in your Resources:
<Window.Resources>
<x:Array Type="sys:String"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Key="MyArray">
<sys:String>One</sys:String>
<sys:String>Two</sys:String>
</x:Array>
</Window.Resources>
...
<Style x:Key="{x:Type ComboBox}"
TargetType="{x:Type ComboBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SomeCheckBox, Path=IsChecked}" Value="True">
<Setter Property="ItemsSource" Value="{StaticResource MyArray}" />
</DataTrigger>
</Style.Triggers>
</Style>
In short: I've got a Style. It uses TemplateBinding a fair bit to make it parametrized instead of repeating myself over and over again. However, when a trigger for that style gets used and a resource gets used in a setter in that trigger, it just doesn't show up! Not even the default value gets shown. Here's a small program that replicates this issue:
TestDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace">
<Style TargetType="Button" x:Key="BtnTest">
<Style.Resources>
<Label Content="{TemplateBinding lcl:TestClass.String}" x:Key="innerLabel"/>
</Style.Resources>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content" Value="{DynamicResource innerLabel}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
MainWindow.xaml
<Window x:Class="MyNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace"
Title="Test" Width="500" Height="350">
<Window.Resources>
<ResourceDictionary Source="TestDictionary.xaml"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Content="Enable/Disable" Click="Click"/>
<Button Grid.Column="1" x:Name="btn" Style="{DynamicResource BtnTest}" lcl:TestClass.String="TESTING"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace MyNamespace
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Click(object sender, RoutedEventArgs e)
{
btn.IsEnabled = !btn.IsEnabled;
}
}
public class TestClass
{
public static string GetString(DependencyObject obj)
{
return (string)obj.GetValue(StringProperty);
}
public static void SetString(DependencyObject obj, string value)
{
obj.SetValue(StringProperty, value);
}
public static readonly DependencyProperty StringProperty =
DependencyProperty.RegisterAttached("String", typeof(string), typeof(TestClass), new PropertyMetadata("Default!"));
}
}
Instead of using a TemplateBinding, I also tried this:
{Binding Path=lcl:TestClass.String, RelativeSource={RelativeSource AncestorType={x:Type Button}}}
It still didn't work.
I know I'm probably doing something wrong, but the question is: what is it?
All you really need to make this work is to use RelativeSource in your binding. Since you are setting the attached property on the Button, in your style trigger, you can just bind to the attached property on self:
<Style TargetType="Button" x:Key="BtnTest">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content"
Value="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource Self}}"/>
</Trigger>
</Style.Triggers>
</Style>
One cool thing about using your approach, since Button is a ContentControl, you're attached property can be any object, not just strings.
And to clarify what went wrong in your previous approach -
As others have said, TemplateBinding only works in ControlTemplates. It also only works when the DependencyProperty is defined on the class you are creating the template for (so you can never do a TemplateBinding to Grid.Row for example)
When binding to an attached property, the whole thing needs to be in parentheses, otherwise WPF will try to bind to a property of a property. Otherwise your RelativeSource binding was close!
I think if you want to have a Label inside the Button as the content, it may work (I didn't test that), but it doesn't seem like the best idea, as your Button can host any object you want.
EDIT for more complex example
So, if you need to display more than one dynamic property, I would recommend using a DataTemplate:
<Style TargetType="Button" x:Key="BtnTest">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Label Content="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource AncestorType={x:Type Button}}}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
Also, I want to point out that a DataTemplateSelector might be more applicable if you have multiple different criteria for changing the look of the content.
Now I see the details. What you should write before relative source is like:
Binding Path=(lcl:TestClass.String)
Do not forget to add parenthesis.
Your example does not work because TemplateBinding only works in a ControlTemplate. To achieve something akin to a TemplateBinding in Resources you need to do other stuff. Here's an example.
In order for TemplateBinding to work, you need to fix the code a little bit (this is just an example with no resources):
<Style x:Key="BtnTest" TargetType="{x:Type Button}">
<Setter Property="MinHeight" Value="100" />
<Setter Property="MinWidth" Value="200" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Blue" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2" Background="{TemplateBinding Background}">
<ContentPresenter RecognizesAccessKey="True" Content="{TemplateBinding lcl:TestClass.String}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</Style.Triggers>
</Style>
Useful links about this topic: Here, and here too.
EDIT:
You can also use the application settings instead of TestClass. Open "Project -> Properties: MyNamespace... -> Settings" and add your settings:
Name--------Type--------Scope--------Value
LabelText---string--------User----------Default
Set the your value for the LabelText in code. For example:
public MainWindow()
{
InitializeComponent();
MyNamespace.Properties.Settings.Default.LabelText = "Testing";
}
And use this ResourceDictionary:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:MyNamespace.Properties"
xmlns:lcl="clr-namespace:MyNamespace">
<Style TargetType="Button" x:Key="BtnTest">
<Style.Resources>
<Label x:Key="innerLabel" Content="{Binding Source={x:Static properties:Settings.Default}, Path=LabelText, Mode=TwoWay}" />
</Style.Resources>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content" Value="{DynamicResource innerLabel}"/>
</Trigger>
</Style.Triggers>
</Style>
I'm trying to create a trigger to disable the combobox drop down button if there are no items. This is the XAML code I've tried so far, however I am unsure of how to detect whether there are no items contained in the ComboBox, and how to disable the button which drops down the list specifically.
<Style TargetType="ComboBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<ControlTemplate.Triggers>
<Trigger Property="Items.Count" Value="0">
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This worked for me:
<ComboBox IsEnabled="{Binding RelativeSource=
{RelativeSource Mode=Self}, Path=ItemsSource.Count}"/>
Assuming whatever you have bound to your ItemsSource property has a count method (it worked for ObservableCollection). It's actually kind of interesting that count being 0 resolves to false in xaml, however this wouldn't be the case in C#.
You can add it to a style if you need to add it to a control programatically
<Style TargetType="ComboBox" x:Key="ComboStyle">
<Setter Property="IsEnabled" Value="{Binding RelativeSource=
{RelativeSource Mode=Self}, Path=ItemsSource.Count}"/>
</Style>
ComboBox cbo = new ComboBox();
cbo.ItemsSource = MyData;
cbo.Style = Resources["ComboStyle"] as Style;
I want to have ListBoxItem trigger two events which I can catch from the outer usercontrol that contains the ListBox. Here is what I got so far:
<ListBox
Background="Black"
Selected="listbox_selected"
x:Name="listBox">
<ListBox.Resources>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver,RelativeSource={RelativeSource Self}}"
Value="True">
<Setter Property="IsSelected" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
Now, this calls my listbox_Selected event. What I want is a calling a different event or property when IsMouseOver. Just to make it clear, I know how to change the background/foreground or other properties of the ListBoxItem itself. But I want to change something of the grandparent.
You already have that event... Handle a static routed event from ListBoxItem class called "Selected" (and there is also "UnSelected") at any ancestor, provided that we dont handle "Selection" event anywhere in the descendents tree ...
<Window x:Class="...."
...
ListBoxItem.Selected="OnListBoxSelected">
<Grid>
<ListBox ItemsSource="{Binding Employees}"
DispalyMemberPath="Name"
selectedValuePath="ID" >
<ListBox.Resources>
<Style TargetType="ListBoxItem"
BasedOn="{StaticResource
{x:Type ListBoxItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver,
RelativeSource={RelativeSource
Self}}"
Value="True">
<Setter Property="IsSelected"
Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
</Grid>
</Window>
And in code behind ...
private void OnListBoxSelected(object sender, RoutedEventArgs e)
{
var window = sender as Window;
var listBoxItem = e.OriginalSource as ListBoxItem;
var selectedItem = listBoxItem.DataContext;
}
Hope this helps...