I have a usercontrol with a DependencyProperty of Answer which is attached to a TextBox.
I have queried the database and bound the answer to the usercontrol and the correct value is displayed.
The issue occurs when I edit the TextBox, the PropertyChanged event is not firing and thus preventing me from saving the correct value back to the database.
Please see below my code.
Usercontrol:
<Grid>
<StackPanel>
<TextBlock Name="txtbQuestion" TextWrapping="Wrap" Text="Question" Foreground="Black" Margin="5" Style="{DynamicResource Label}" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}" ></TextBlock>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<TextBox Name="txtAnswer" Margin="5" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" >
<TextBox.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=cbMultiLine, Path=IsChecked}" Value="True">
<Setter Property="TextBox.TextWrapping" Value="Wrap" />
<Setter Property="TextBox.Height" Value="100" />
<Setter Property="TextBox.AcceptsReturn" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox Name="cbMultiLine" Content="MultiLine" Margin="5" FontFamily="Georgia" Grid.Column="1" />
</Grid>
<Line Fill="Black" Margin="4" />
</StackPanel>
</Grid>
Usercontrol.cs:
public partial class ConditionQuestion : UserControl
{
public static readonly DependencyProperty AnswerProperty =
DependencyProperty.Register("Answer", typeof(string), typeof(ConditionQuestion), new UIPropertyMetadata(null, Answer_PropertyChanged));
public static readonly DependencyProperty QuestionProperty =
DependencyProperty.Register("Question", typeof(string), typeof(ConditionQuestion), new UIPropertyMetadata(null, Question_PropertyChanged));
public ConditionQuestion()
{
InitializeComponent();
}
public string Answer
{
get { return (string)GetValue(AnswerProperty); }
set { SetValue(AnswerProperty, value); }
}
public string Question
{
get { return (string)GetValue(QuestionProperty); }
set { SetValue(QuestionProperty, value); }
}
private static void Answer_PropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
((ConditionQuestion)source).txtAnswer.Text = (string)e.NewValue;
}
private static void Question_PropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
((ConditionQuestion)source).txtbQuestion.Text = (string)e.NewValue;
}
}
Declaring instance of Usercontrol:
<ListBox ItemContainerStyle="{StaticResource noSelect}" ItemsSource="{Binding Answer}"
Name="lbQuestions" BorderBrush="#E6E6E6" >
<ListBox.ItemTemplate>
<DataTemplate>
<my:ConditionQuestion Question="{Binding ConditionReportFormQuestions.Question}"
Answer="{Binding Answer, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I apologize in advance, I am {relatively} new to WPF. Can anyone see where I may be going wrong with this?
I have successfully got my other usercontrols binding and updating (this code is almost an exact copy) but the answers on them are ListBox selections where-as this usercontrol is binding to a TextBox.
Thanks in advance,
Kohan.
You have not bound the text box to the answer property. What you have done is put a changed handler on your answer property and when it is changed you manually set the text boxes text property.
Your code should look something like this
<TextBlock
Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type my:ConditionQuestion}}, Path=Answer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
this is a binding between the textbox and the property Answer on the class ConditionQuestion (the user control). Whenever the Answer property changes on the user control the text box will update and whenever you change the text in the textbox the Answer property will be updated. With this code you can remove your Answer_PropertyChanged method it is no longer neccessary. the binding takes care of it
Related
I have created a custom control to search a customer in a list or by entering the customer code directly:
When a user enters a customer code and the textbox loses focus, an event should trigger. I can get this to work on a standalone control but as soon as I put the XAML in a template, the LostFocus event doesn't trigger anymore.
I've read somewhere that the Interaction Triggers don't work in styles/templates. What is the correct way to create a reusable MVVM control with LostFocus functionality on an underlying TextBox?
CustomerSelectorField.cs
public class CustomerSelectorField : Control
{
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
// Using a DependencyProperty as the backing store for Command. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(CustomerSelectorField), new PropertyMetadata(null));
public ICommand LostFocusCommand
{
get { return (ICommand)GetValue(LostFocusCommandProperty); }
set { SetValue(LostFocusCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for LostFocusCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LostFocusCommandProperty =
DependencyProperty.Register("LostFocusCommand", typeof(ICommand), typeof(CustomerSelectorField), new PropertyMetadata(null));
}
XAML
<Style TargetType="{x:Type custom_controls:CustomerSelectorField}">
<Setter Property="FocusManager.IsFocusScope" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type custom_controls:CustomerSelectorField}">
<Grid wpf:MarginSetter.Margin="0,0,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding CustomerCode}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{TemplateBinding LostFocusCommand}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<custom_controls:IconButton
Grid.Column="1"
Command="{TemplateBinding Command}"
CommandParameter="{Binding}" />
<TextBox IsReadOnly="True" Text="{Binding Name, Mode=OneWay}" Grid.Column="2" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Implementation
<custom_controls:CustomerSelectorField DataContext="{Binding ParentAcc}" LostFocusCommand="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.ParentCustomerLostFocusCommand}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.OpenParentCustomerSelectorCommand}" />
I've used a data template in these scenarios:-
<DataTemplate x:Key="foo"
DataType="{x:Type custom_controls:CustomerSelectorField}">
<Grid wpf:MarginSetter.Margin="0,0,5,0">
...
</Grid>
</DataTemplate>
(You'll need to change the InvokeCommandAction binding from "{TemplateBinding ...}" to "{Binding ...}").
Your control's XAML stays the same but you need to wrap it in a ContentControl then apply the data template to that:-
<ContentControl ContentTemplate="{StaticResource foo}">
<custom_controls:CustomerSelectorField ....
</ContentControl>
It's not ideal but it does the job.
I want to change the Style of a TextBlock if the value of the bound property is null. I have specified a value for TargetNullValue of the TextBlock to be displayed, but i want to display it with an alternativ style. How can i do it.
My current solution is to use two TextBlocks and control the Visibility of both, to toggle between original and alternativ style. But this solution is not viable, cause i need to duplicate each TextBlock, for displaying alternativ version.
Current Solution:
<TextBlock Visibility="{Binding MyText, Converter={StaticResource nullToVisibilityConverter}}"
FontSize="20"
Foreground="Black"
Text="{Binding MyText}" />
<TextBlock Visibility="{Binding MyText, Converter={StaticResource nullToVisibilityConverter}}"
FontSize="20"
FontStyle="Italic"
Foreground="Gray"
Text="None" />
Needed Solution:
<TextBlock FontSize="20"
Foreground="Black"
Text="{Binding MyText, TargetNullValue='None'}" />
<!-- plus any styles, templates or triggers, to change style of TextBlock for TargetNullValue -->
How can i use an alternativ style for a TargetNullValue. Any solutions using Styles, Triggers or Templates are welcome.
Note: this is for WPF, you might have to convert anything that doesn't quite mesh with SL5.0.
The easiest solution (unless this requirement is ubiquitous) is to make a specific style for each instance which binds to the same property as the textblock, checks for Null and sets properties there.
This example will paste nicely into Kaxaml.
<Style x:Key="tacoStyle" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource taco}}" Value="{x:Null}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<TextBlock Style="{StaticResource tacoStyle}" Text="{Binding Source={StaticResource taco}, TargetNullValue='bacon'}"/>
Of course if you need a more generic solution, you could extend TextBlock and add some logic to handle this.
<StackPanel>
<StackPanel.Resources>
<x:NullExtension x:Key="taco"/>
<Style x:Key="AltTacoStyle" TargetType="yourNS:ExtendedTextBlock">
<Setter Property="Foreground" Value="Pink"/>
</Style>
</StackPanel.Resources>
<yourNS:ExtendedTextBlock Text="{Binding Source={StaticResource taco}, TargetNullValue='bacon'}"
AltStyle="{StaticResource AltTacoStyle}"
AllowAltStyleOnNull="True"/>
</StackPanel>
And the control:
public class ExtendedTextBlock : TextBlock {
public static readonly DependencyProperty AllowAltStyleOnNullProperty =
DependencyProperty.Register("AllowAltStyleOnNull", typeof (bool), typeof (ExtendedTextBlock), new PropertyMetadata(default(bool)));
public bool AllowAltStyleOnNull {
get { return (bool) GetValue(AllowAltStyleOnNullProperty); }
set { SetValue(AllowAltStyleOnNullProperty, value); }
}
public static readonly DependencyProperty AltStyleProperty =
DependencyProperty.Register("AltStyle", typeof (Style), typeof (ExtendedTextBlock), new PropertyMetadata(default(Style)));
public Style AltStyle {
get { return (Style) GetValue(AltStyleProperty); }
set { SetValue(AltStyleProperty, value); }
}
static ExtendedTextBlock() {
TextProperty.OverrideMetadata(typeof(ExtendedTextBlock), new FrameworkPropertyMetadata(default(string), PropertyChangedCallback));
}
private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) {
var tb = (ExtendedTextBlock)dependencyObject;
var binding = tb.GetBindingExpression(TextProperty);
if (binding != null && binding.DataItem == null) {
if (tb.AllowAltStyleOnNull)
tb.Style = tb.AltStyle;
}
}
}
You'll need to flesh that out a bit to be production-ready, but you get the idea.
This is my attached property:
public class MyButtonThing
{
public static string GetText2(DependencyObject obj)
{
return (string)obj.GetValue(Text2Property);
}
public static void SetText2(DependencyObject obj, string value)
{
obj.SetValue(Text2Property, value);
}
public static readonly DependencyProperty Text2Property =
DependencyProperty.RegisterAttached("Text2",
typeof(string), typeof(System.Windows.Controls.Button));
}
This is my ControlTemplate:
EDIT this will work fine:
<Window.Resources>
<ControlTemplate TargetType="{x:Type Button}"
x:Key="MyButtonTemplate">
<Border>
<DockPanel LastChildFill="True">
<TextBlock Text="{TemplateBinding Content}"
DockPanel.Dock="Top"/>
<TextBlock Text={Binding RelativeSource={RelativeSource
AncestorType=Button},
Path=(local:MyButtonThing.Text2)}" />
</DockPanel>
</Border>
</ControlTemplate>
</Window.Resources>
<Button Template="{StaticResource MyButtonTemplate}"
local:MyButtonThing.Text2="Where's Waldo"
>Hello World</Button>
My problem? Text2 renders properly in the Desginer, not at runtime.
You set the value on the button, and it is attached, hence:
{Binding RelativeSource={RelativeSource AncestorType=Button},
Path=(local:MyButtonThing.Text2)}
You are binding to the DataContext of TextBox, which doesn't have a Text2 property
Use this instead:
<TextBlock Text="{Binding RelativeSource={RelativeSource Self},
Path=local:MyButtonThing.Text2}" />
It sets the TextBox's DataContext to the TextBox Control, not the TextBox's DataContext
I have a WPF Combobox which is filled with, say, Customer objects. I have a DataTemplate:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
This way, when I open my ComboBox, I can see the different Customers with their Name and, below that, the Address.
But when I select a Customer, I only want to display the Name in the ComboBox. Something like:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
Can I select another Template for the selected item in a ComboBox?
Solution
With help from the answers, I solved it like this:
<UserControl.Resources>
<ControlTemplate x:Key="SimpleTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="ExtendedTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="CustomerTemplate">
<Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</UserControl.Resources>
Then, my ComboBox:
<ComboBox ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}"
ItemTemplate="{StaticResource CustomerTemplate}" />
The important part to get it to work was Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (the part where value should be x:Null, not True).
The issue with using the DataTrigger/Binding solution mentioned above are two-fold. The first is you actually end up with a binding warning that you can't find the relative source for the selected item. The bigger issue however is that you've cluttered up your data templates and made them specific to a ComboBox.
The solution I present better follows WPF designs in that it uses a DataTemplateSelector on which you can specify separate templates using its SelectedItemTemplate and DropDownItemsTemplate properties as well as ‘selector’ variants for both.
Note: Updated for C#9 with nullability enabled and using pattern matching during the search
public class ComboBoxTemplateSelector : DataTemplateSelector {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or
// a ComboBoxItem (or null). This will determine which template to use
while(itemToCheck is not null
and not ComboBox
and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown
var inDropDown = itemToCheck is ComboBoxItem;
return inDropDown
? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
: SelectedItemTemplate ?? SelectedItemTemplateSelector?.SelectTemplate(item, container);
}
}
To make it easier to use in XAML, I've also included a markup extension that simply creates and returns the above class in its ProvideValue function.
public class ComboBoxTemplateSelectorExtension : MarkupExtension {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
=> new ComboBoxTemplateSelector(){
SelectedItemTemplate = SelectedItemTemplate,
SelectedItemTemplateSelector = SelectedItemTemplateSelector,
DropdownItemsTemplate = DropdownItemsTemplate,
DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
};
}
And here's how you use it. Nice, clean and clear and your templates stay 'pure'
Note: 'is:' here is my xmlns mapping for where I put the class in code. Make sure to import your own namespace and change 'is:' as appropriate.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />
You can also use DataTemplateSelectors if you prefer...
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Or mix and match! Here I'm using a template for the selected item, but a template selector for the DropDown items.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Additionally, if you don't specify a Template or a TemplateSelector for the selected or dropdown items, it simply falls back to the regular resolving of data templates based on data types, again, as you would expect. So, for instance, in the below case, the selected item has its template explicitly set, but the dropdown will inherit whichever data template applies for the DataType of the object in the data context.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MyTemplate} />
Enjoy!
Simple solution:
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Address}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
(Note that the element that is selected and displayed in the box and not the list is not inside a ComboBoxItem hence the trigger on Null)
If you want to switch out the whole template you can do that as well by using the trigger to e.g. apply a different ContentTemplate to a ContentControl. This also allows you to retain a default DataType-based template selection if you just change the template for this selective case, e.g.:
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
Value="{x:Null}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<!-- ... -->
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ComboBox.ItemTemplate>
Note that this method will cause binding errors as the relative source is not found for the selected item. For an alternate approach see MarqueIV's answer.
In addition to what said by H.B. answer, the Binding Error can be avoided with a Converter. The following example is based from the Solution edited by the OP himself.
The idea is very simple: bind to something that alway exists (Control) and do the relevant check inside the converter.
The relevant part of the modified XAML is the following. Please note that Path=IsSelected was never really needed and ComboBoxItem is replaced with Control to avoid the binding errors.
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
The C# Converter code is the following:
public class ComboBoxItemIsSelectedConverter : IValueConverter
{
private static object _notNull = new object();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// value is ComboBox when the item is the one in the closed combo
if (value is ComboBox) return null;
// all the other items inside the dropdown will go here
return _notNull;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I was going to suggest using the combination of an ItemTemplate for the combo items, with the Text parameter as the title selection, but I see that ComboBox doesn't respect the Text parameter.
I dealt with something similar by overriding the ComboBox ControlTemplate. Here's the MSDN website with a sample for .NET 4.0.
In my solution, I change the ContentPresenter in the ComboBox template to have it bind to Text, with its ContentTemplate bound to a simple DataTemplate that contains a TextBlock like so:
<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
<TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>
with this in the ControlTemplate:
<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>
With this binding link, I am able to control the Combo selection display directly via the Text parameter on the control (which I bind to an appropriate value on my ViewModel).
I used next approach
<UserControl.Resources>
<DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
<TextBlock Text="{Binding Path=ShortName}" />
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<ComboBox DisplayMemberPath="FullName"
ItemsSource="{Binding Path=Offsets}"
behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
SelectedItem="{Binding Path=Selected}" />
<TextBlock Text="User Time" />
<TextBlock Text="" />
</StackPanel>
And the behavior
public static class SelectedItemTemplateBehavior
{
public static readonly DependencyProperty SelectedItemDataTemplateProperty =
DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));
public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
{
element.SetValue(SelectedItemDataTemplateProperty, value);
}
public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
{
return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uiElement = d as ComboBox;
if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
{
uiElement.Loaded -= UiElementLoaded;
UpdateSelectionTemplate(uiElement);
uiElement.Loaded += UiElementLoaded;
}
}
static void UiElementLoaded(object sender, RoutedEventArgs e)
{
UpdateSelectionTemplate((ComboBox)sender);
}
private static void UpdateSelectionTemplate(ComboBox uiElement)
{
var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
if (contentPresenter == null)
return;
var template = uiElement.GetSelectedItemDataTemplate();
contentPresenter.ContentTemplate = template;
}
public static T GetChildOfType<T>(DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want
Yes. You use a Template Selector to determine which template to bind at run-time. Thus if IsSelected = False then Use this template, if IsSelected = True, use this other template.
Of Note:
Once you implement your template selector, you will need to give the templates keynames.
I propose this solution without DataTemplateSelector, Trigger, binding nor behavior.
The first step is to put the ItemTemplate (of the selected element) in the ComboBox resources and the ItemTemplate (of the item in the drop down menu) in the ComboBox.ItemsPanel resources and give both resources the same key.
The second step is to postpone the ItemTemplate resolution at run time by using both a ContentPresenter and a DynamicResource in the actual ComboBox.ItemTemplate implementation.
<ComboBox ItemsSource="{Binding Items, Mode=OneWay}">
<ComboBox.Resources>
<!-- Define ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<TextBlock Text="{Binding FieldOne, Mode=OneWay}" />
</DataTemplate>
</ComboBox.Resources>
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Grid.IsSharedSizeScope="True"
IsItemsHost="True">
<StackPanel.Resources>
<!-- Redefine ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupOne" />
<ColumnDefinition Width="10" SharedSizeGroup="GroupSpace" />
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupTwo" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding FieldOne, Mode=OneWay}" />
<TextBlock Grid.Column="2" Text="{Binding FieldTwo, Mode=OneWay}" />
</Grid>
</DataTemplate>
</StackPanel.Resources>
</StackPanel>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentPresenter ContentTemplate="{DynamicResource ItemTemplate}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Although somewhat experienced with writing Winforms applications, the... "vagueness" of WPF still eludes me in terms of best practices and design patterns.
Despite populating my list at runtime, my listbox appears empty.
I have followed the simple instructions from this helpful article to no avail. I suspect that I'm missing some sort of DataBind() method where I tell the listbox that I'm done modifying the underlying list.
In my MainWindow.xaml, I have:
<ListBox ItemsSource="{Binding TopicList}" Height="177" HorizontalAlignment="Left" Margin="15,173,0,0" Name="listTopics" VerticalAlignment="Top" Width="236" Background="#0B000000">
<ListBox.ItemTemplate>
<HierarchicalDataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsChecked}"/>
</HierarchicalDataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In my code-behind, I have:
private void InitializeTopicList( MyDataContext context )
{
List<Topic> topicList = ( from topic in context.Topics select topic ).ToList();
foreach ( Topic topic in topicList )
{
CheckedListItem item = new CheckedListItem();
item.Name = topic.DisplayName;
item.ID = topic.ID;
TopicList.Add( item );
}
}
Which, by tracing through, I know is being populated with four items.
EDIT
I have changed TopicList to an ObservableCollection. It still doesn't work.
public ObservableCollection<CheckedListItem> TopicList;
EDIT #2
I have made two changes that help:
In the .xaml file:
ListBox ItemsSource="{Binding}"
In the source code after I populate the list:
listTopics.DataContext = TopicList;
I'm getting a list, but it's not automagically updating the checkbox states when I refresh those. I suspect a little further reading on my part will resolve this.
Assuming TopicList is not an ObservableCollection<T> therefore when you add items no INotifyCollection changed is being fired to tell the binding engine to update the value.
Change your TopicList to an ObservableCollection<T> which will resolve the current issue. You could also populate the List<T> ahead of time and then the binding will work via OneWay; however ObservableCollection<T> is a more robust approach.
EDIT:
Your TopicList needs to be a property not a member variable; bindings require properties. It does not need to be a DependencyProperty.
EDIT 2:
Modify your ItemTemplate as it does not need to be a HierarchicalDataTemplate
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsChecked}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
Use ObservableCollection<Topic> instead of List<Topic>
Edit
it implements INotifyCollectionChanged interface to let WPF know when you add/remove/modify items
Edit 2
Since you set TopicList in code, it should be a Dependency Property, not a common field
public ObservableCollection<CheckedListItem> TopicList {
get { return (ObservableCollection<CheckedListItem>)GetValue(TopicListProperty); }
set { SetValue(TopicListProperty, value); }
}
public static readonly DependencyProperty TopicListProperty =
DependencyProperty.Register("TopicList", typeof(ObservableCollection<CheckedListItem>), typeof(MainWindow), new UIPropertyMetadata(null));
Edit 3
To see changes in items
implement INotifyPropertyChanged interface in CheckedListItem (each setter should call PropertyChanged(this, new PropertyChangedEventArgs(<property name as string>)) event)
or derive CheckedListItem from DependencyObject, and convert Name, ID, IsChecked to dependency properties
or update them totally (topicList[0] = new CheckedListItem() { Name = ..., ID = ... })
First you dont need a HeirarchicalDataTemplate for this. Just regular DataTemplate as Aaron has given is enough.
Then you need to instantiate the TopicList ObservableCollection somewhere inside the constructor of the class. which makes the ObservableCollection alive even before you add data in to it And binding system knows the collection. Then when you add each and every Topic/CheckedListItem it will automatically shows up in the UI.
TopicList = new ObservableCollection<CheckedListItem>(); //This should happen only once
private void InitializeTopicList( MyDataContext context )
{
TopicList.Clear();
foreach ( Topic topic in topicList )
{
CheckedListItem item = new CheckedListItem();
item.Name = topic.DisplayName;
item.ID = topic.ID;
TopicList.Add( item );
}
}
Others have already made useful suggestions (use an observable collection to get list-change notification, make the collection a property rather than a field). Here are two they haven't:
1) Whenever you're having a problem with data binding, look in the Output window to make sure that you're not getting any binding errors. You can spend a lot of time trying to fix the wrong problem if you don't do this.
2) Understand the role change notification plays in binding. Changes in your data source can't and won't get propagated to the UI unless the data source implements change notification. There are two ways to do this for normal properties: make the data source derive from DependencyObject and make the bound property a dependency property, or make the data source implement INotifyPropertyChanged and raise the PropertyChanged event when the property's value changes. When binding an ItemsControl to a collection, use a collection class that implements INotifyCollectionChanged (like ObservableCollection<T>), so that changes to the contents and order of the collection will get propagated to the bound control. (Note that if you want changes to the items in the collection to get propagated to the bound controls, those items need to implement change notification too.)
I know this is really old question but I came to building custom Listbox which get the SelectedItems with built in select all / unselect all
CustomListBox
public class CustomListBox : ListBox
{
#region Constants
public static new readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CustomListBox), new PropertyMetadata(default(IList), OnSelectedItemsPropertyChanged));
#endregion
#region Properties
public new IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
#endregion
#region Event Handlers
private static void OnSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CustomListBox)d).OnSelectedItemsChanged((IList)e.OldValue, (IList)e.NewValue);
}
protected virtual void OnSelectedItemsChanged(IList oldSelectedItems, IList newSelectedItems)
{
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetValue(SelectedItemsProperty, base.SelectedItems);
}
#endregion
}
ListBoxControl.cs
public partial class ListBoxControl : UserControl
{
#region Constants
public static new readonly DependencyProperty ContentProperty =
DependencyProperty.Register(nameof(Content), typeof(object), typeof(ListBoxControl),
new PropertyMetadata(null));
public static new readonly DependencyProperty ContentTemplateProperty =
DependencyProperty.Register(nameof(ContentTemplate), typeof(DataTemplate), typeof(ListBoxControl),
new PropertyMetadata(null));
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(nameof(Items), typeof(IList), typeof(ListBoxControl),
new PropertyMetadata(null));
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(ListBoxControl),
new UIPropertyMetadata(null, OnSelectedItemsChanged));
#endregion
#region Properties
public new DataTemplate ContentTemplate
{
get => (DataTemplate)GetValue(ContentTemplateProperty);
set => SetValue(ContentTemplateProperty, value);
}
public IList Items
{
get => (IList)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
#endregion
#region Constructors
public ListBoxControl()
{
InitializeComponent();
}
#endregion
#region Event Handlers
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListBoxControl || e.NewValue is not IList newValue)
{
return;
}
var mylist = (d as ListBoxControl).CustomList;
foreach (var selectedItem in newValue)
{
mylist.UpdateLayout();
if (mylist.ItemContainerGenerator.ContainerFromItem(selectedItem) is ListBoxItem selectedListBoxItem)
{
selectedListBoxItem.IsSelected = true;
}
}
}
#endregion
#region Private Methods
private void CheckAll_Click(object sender, RoutedEventArgs e)
{
CustomList.SelectAll();
}
private void UncheckAll_Click(object sender, RoutedEventArgs e)
{
CustomList.UnselectAll();
}
#endregion
}
#endregion
ListBoxControl.xaml
<UserControl x:Class="UserControls.ListBoxControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:UserControls"
xmlns:str="Client.Properties"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="this">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</UserControl.Resources>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:CustomListBox x:Name="CustomList"
Grid.Row="0"
Width="250"
HorizontalAlignment="Left"
SelectionMode="Multiple"
Visibility="Visible"
MinHeight="25"
MaxHeight="400"
ItemsSource="{Binding ElementName=this, Path =Items}"
SelectedItems="{Binding ElementName=this, Path =SelectedItems,Mode=TwoWay}"
Style="{StaticResource {x:Type ListBox}}"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<local:CustomListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Trigger>
<Trigger Property="IsMouseCaptureWithin" Value="true">
<Setter Property="IsSelected" Value="true" />
</Trigger>
<Trigger Property="IsMouseCaptureWithin" Value="False">
<Setter Property="IsSelected" Value="False" />
</Trigger>
</Style.Triggers>
</Style>
</local:CustomListBox.ItemContainerStyle>
<local:CustomListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<CheckBox Margin="4" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}},Path=IsSelected}" />
<ContentPresenter Content="{Binding .}" ContentTemplate="{Binding ElementName=this, Path = ContentTemplate, Mode=OneWay}"/>
</DockPanel>
</DataTemplate>
</local:CustomListBox.ItemTemplate>
</local:CustomListBox>
<Grid Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Left">
<Button Click="CheckAll_Click"
BorderBrush="Transparent"
ToolTip="Check all">
<Button.Content>
<Image Source="CheckAll.png" Height="16" Width="16"/>
</Button.Content>
</Button>
<Button
Click="UncheckAll_Click"
BorderBrush="Transparent"
Visibility="Visible"
ToolTip="Unchecked all">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=this, Path = SelectedItems.Count}" Value="0">
<Setter Property="Button.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<Button.Content>
<Image Source="UncheckAll.png" Height="16" Width="16" />
</Button.Content>
</Button>
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding ElementName=this, Path = SelectedItems.Count, StringFormat={x:Static str:Resources.STE_LABEL_X_ITEMS_CHECKED}, Mode=OneWay}"
HorizontalAlignment="Right" TextAlignment="Right" VerticalAlignment="Center"
Foreground="White" />
</Grid>
</Grid>
</UserControl>
Now you can use that custom control in any control or page and pass any content you want
EX : ConfigView.xaml
<UserControl ..
xmlns:userControls="Client.UserControls"
..>
<userControls:ListBoxControl
ShowCheckBox="True"
MinHeight="25"
MaxHeight="400"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Items="{Binding MyLists, Mode=OneWay}"
SelectedItems="{Binding SelectedMyLists,Mode=TwoWay}"
HorizontalAlignment="Left">
<userControls:ListBoxControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<Image Source="{Binding Icon}"/>
<TextBlock VerticalAlignment="Center" Text="{Binding Name,StringFormat=' {0}'}" />
</StackPanel>
</DataTemplate>
</userControls:ListBoxControl.ContentTemplate>
</userControls:ListBoxControl>
here we bind to the selected items and than do explicit casting to our model
ConfigViewViewModel
private IList _myLists;
public IList MyLists
{
get => _myLists;
set
{
if (_myLists == value)
{
return;
}
_myLists = value;
OnPropertyChanged(nameof(SelectedItems));
}
}
public IEnumerable<MyModel> SelectedItems => MyLists.Cast<MyModel>();
change your binding to
<ListBox ItemsSource="{Binding Path=TopicList}"