WPF: Custom ListBoxItem with DataTrigger - c#

I have a WPF ListBox containing CheckBoxes. I would like the text colour of the TextBox to change to red when the ViewModel notices that the bound value is now updated. I have the below XAML but it is not working. I can see the IsUpdated property being queried but when the value is True the colour is not changing. I'm sure I'm missing something obvious but can't quite figure it out.
<ListBox MinHeight="100" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="2" SnapsToDevicePixels="true">
<CheckBox x:Name="_checkBox" IsChecked="{Binding Path=IsAllowed}" Content="{Binding Item}"/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsUpdated}" Value="True">
<Setter TargetName="_checkBox" Property="Foreground" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Are you implementing INotifyPropertyChanged (as mentioned by Matt Hamilton) in your Item class and raising the PropertyChanged event when you set IsUpdated from false to true and vice-versa.
public class Item : INotifyPropertyChanged
{
// ...
private bool _isUpdated;
public bool IsUpdated
{
get{ return _isUpdated; }
set {
_isUpdated= value;
RaisePropertyChanged("IsUpdated");
}
}
// ...
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if(PopertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
// ...
}

Related

DataGrid columns do not refresh values with DataGrid2D

I am trying to bind columns and rows of DataGrid to the same source but DataGrid does not show new value in the ColumnHeader when I change value in the RowHeader after TextBox lost focus. Headers collection contains new values as expected.
xmlns:dataGrid2D="http://gu.se/DataGrid2D"
<Grid>
<DataGrid
dataGrid2D:ItemsSource.RowHeadersSource="{Binding Headers}"
dataGrid2D:ItemsSource.ColumnHeadersSource="{Binding Headers}"
dataGrid2D:ItemsSource.Array2D="{Binding Items}"
IsReadOnly="True"
AutoGenerateColumns="True">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBox Text="{Binding Value}" />
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
</Grid>
DataContext:
public class MainViewModel
{
public IEnumerable<Header> Headers { get; } = new Header[]
{ new Header { Value = "1" }, new Header { Value = "2" } };
public string[,] Items { get; } = new string[2, 2] { { "1", "2" }, { "3", "4" } };
}
public class Header
{
public string Value { get; set; }
public override string ToString()
{
return Value;
}
}
What I am doing wrong?
I am changing the Value property from the View side so it does not need INotifyPropertyChanged interface. If I call "Refresh" on DataGrid it updates but with blinking.
Edit:
Why are you downvoting me? Is it a bad question?
Here is one possible solution using the style for DataGridColumnHeader. I also explicitly used the Value property instead of the ToString method.
<Grid>
<DataGrid
dataGrid2D:ItemsSource.RowHeadersSource="{Binding Headers}"
dataGrid2D:ItemsSource.ColumnHeadersSource="{Binding Headers}"
dataGrid2D:ItemsSource.Array2D="{Binding Items}"
IsReadOnly="True"
AutoGenerateColumns="True">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding Value}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
</Grid>
You need to slightly change the ModelView then:
public class Header: INotifyPropertyChanged
{
private string _Value;
public string Value
{
get
{
return _Value;
}
set
{
_Value = value;
OnPropertyChanged("Value");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}

WPF selecting an Item in a ListBox from the ViewModel using SelectedIndex

I'm new to MVVM/WPF and after a few hours of research, not finding any really useful/working answer for my project I decided to give it a go and try asking here.
I would like to select an Item from my Listbox, which uses a List as an ItemSource.
Relevant ViewModel:
public class FavoriteStructureVm : INotifyPropertyChanged
{
#region
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
public ObservableCollection<FavoriteDataVm> Favorites { get; set; }
public int SelectedIndex { get; set; }
private FavoriteDataVm _selectedItem;
public FavoriteDataVm SelectedItem
{
set
{
_selectedItem = value;
var item = (FavoriteDataVm)_selectedItem;
if (item.Type == FavoriteDataType.Add)
{
SelectedIndex = 1;
}
}
}
}
The ListBox contains a few items by default, the last one always being one of the type Add, which if selected, may add a new item and select it by default or select the previously selected item if no new item is added. For Simplicity the selected item will just be 1 regardless of wether a new item is added or not.
No matter where and what I tried to update with OnPropertyChanged it didn't update the SelectedIndex in the view, however, by adding/inserting a new FavoriteDataVm into ObservableCollection<FavoriteDataVm> Favorites, the SelectedIndex in the view gets updated.
The process of adding a new item to the list does not always occur, nonetheless I'd like to always update the SelectedIndex.
Relevant XAML:
<ListBox Name="favMenu" ItemsSource="{Binding Favorites}" SelectionMode="Single"
HorizontalAlignment="Center" VerticalAlignment="Top"
BorderThickness="0" Background="Transparent" Height="{Binding ElementName=window, Path=ActualHeight}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}"
>
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="Transparent" SnapsToDevicePixels="true">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<!--changing default orientation-->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="Border"
BorderThickness="0" BorderBrush="Black" Background="{x:Null}"
Width="60" Height="60" CornerRadius="30" Margin="{Binding Margin}"
ToolTip="{Binding Name}">
<Image Source="{Binding ImageUri}" Width="60" Height="60" Stretch="UniformToFill">
<Image.Clip>
<EllipseGeometry RadiusX="30" RadiusY="30" Center="30,30"/>
</Image.Clip>
</Image>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I found a workaround to just create a dummy item and remove it, since adding something seems to update the SelectedIndexin the view. I don't see it as a solution as it comes with a lot of drawbacks.
So this kinda raises two questions actually:
How do I get to update the SelectedIndex of the ListBox?
And a beginner-question, since I'm new to MVVM:
Is this a correct implementation of MVVM?
How do I get to update the SelectedIndex of the ListBox?
You can get the selected index by getting the index of the SelectedItem in the source collection:
int selectedIndex = (SelectedItem != null && Favorites != null) ? Favorites.IndexOf(SelectedItem) : -1;
And you can select an item by setting the SelectedItem property:
SelectedItem = Favorites[1]; //selects the second item
You shouldn't bind both the SelectedItem and the SelectedIndex properties of the ListBox. These must be synchronized. Remove SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" from your XAML.
Also note that if you intend to update a source property dynamically, you need to raise the PropertyChanged for this property for the view to refresh:
private int _selectedIndex;
public int SelectedIndex
{
get { return _selectedIndex; }
set { _selectedIndex = value; OnPropertyChanged("SelectedIndex"); }
}
private FavoriteDataVm _selectedItem;
public FavoriteDataVm SelectedItem
{
set
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
You are not updating the SelectedIndex
The declaration should be as follows
private int _selectedIndex ;
public int SelectedIndex{
get{return _selectedIndex };
set{_selectedIndex=value;OnPropertyChanged("SelectedIndex") };
}
This way the list will be notified when the selectedindex changes
"Sorry if there are any typos, i wrote this directly here and not in a code editor so you might get typos"

Disable comboxboxitem by binding to data bool

I have a ComboBox in a UWP project. I am binding the DataSource to a collection of MyItem class. My class looks like this:
public class MyItem : INotifyPropertyChanged
{
#region INPC
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private string _ItemName;
public string ItemName
{
get { return _ItemName; }
set
{
if (value != _ItemName)
{
_ItemName = value;
Notify("ItemName");
}
}
}
private bool _ItemEnabled;
public bool ItemEnabled
{
get { return _ItemEnabled; }
set
{
if (value != _ItemEnabled)
{
_ItemEnabled = value;
Notify("ItemEnabled");
}
}
}}
Now I want the ComboBoxItem to be enabled or disabled depending on my ItemEnabled property. I researched and tried adding a binding through the Style tag but the binding does not function in UWP.
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding ItemEnabled}" />
</Style>
</ComboBox.ItemContainerStyle>
How can I solve this?
EDIT 1: Binding code
<ComboBox ItemsSource="{Binding Path=MyItemsCollection, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ItemName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Just remove this line (ItemsSource="{Binding Path=MyItemsCollection, UpdateSourceTrigger=PropertyChanged}") in XAML and add this line after InitializeComponent(); in the code behind:
<ComboBox Name="cmb1">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ItemName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding ItemEnabled}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
And in xaml.cs:
public Window1()
{
InitializeComponent();
cmb1.ItemsSource = MyItemsCollection;
}
Edit: Another way is like this:
public Window1()
{
InitializeComponent();
this.DataContext = MyItemsCollection;
}
And in the xaml:
<ComboBox Name="cmb1" ItemsSource="{Binding}">
....

listbox isSelected databinding in DataTemplate

I try to simply databind IsSelected property with IsSelected field in my class. But after I change the value in code, it doesn't change the property, neither does clicking on ListBoxItem change the field value.
XAML:
<FlipView ItemsSource="{Binding Source={StaticResource itemsViewSource}}" ... >
<FlipView.ItemTemplate>
<DataTemplate>
<UserControl Loaded="StartLayoutUpdates"
Unloaded="StopLayoutUpdates">
<!-- other controls -->
<ListBox Grid.Row="1" Grid.ColumnSpan="3"
SelectionMode="Multiple" VerticalAlignment="Center"
ItemsSource="{Binding Answers}">
<ListBox.Resources>
<local:LogicToText x:Key="logToText" />
</ListBox.Resources>
<!-- bind IsSelected only in one way from
code to content -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<ListBoxItem
IsSelected="{Binding IsSelected, Mode=TwoWay, Converter={StaticResource logToText}}"
Content="{Binding IsSelected, Mode=TwoWay, Converter={StaticResource logToText}}">
</ListBoxItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
<!-- not working at all
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="Content"
Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.Resources>-->
</ListBox>
</UserControl>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
Code:
Answers
private ObservableCollection<PrawoJazdyDataAnswer> _answers =
new ObservableCollection<PrawoJazdyDataAnswer>();
public ObservableCollection<PrawoJazdyDataAnswer> Answers
{
get
{
return this._answers;
}
}
Single item(Answer)
public class PrawoJazdyDataAnswer : NPCHelper// PrawoJazdy.Common.BindableBase
{
public PrawoJazdyDataAnswer(String ans, bool ansb)
{
this._ans = ans;
this._isSelected = ansb;
}
public override string ToString()
{
return _isSelected.ToString(); //Only For debug purposes
//normally return _ans
}
private string _ans;
public string Ans
{
get { return this._ans; }
//set { this.SetProperty(ref this._ans, value); }
}
private bool _isSelected;
public bool IsSelected
{
get { return this._isSelected; }
set
{
_isSelected = value;
FirePropertyChanged("IsSelected");
//this.SetProperty(ref this._isSelected, value);
}
}
}
FirePropertyChanged
public class NPCHelper : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void FirePropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Converter(which sometimes seems to be needed and others not..., I tried ~10 approaches from different tutorials/examples)
public class LogicToText : IValueConverter
{
/// <summary>
///
/// </summary>
public object Convert(object value, Type targetType,
object parameter, string language)
{
//if (value == null || (bool)value == false)
// return "False";
return value.ToString();
}
/// <summary>
///
/// </summary>
public object ConvertBack(object value, Type targetType,
object parameter, string language)
{
return value.ToString().Contains("True") ? true : false;
}
Thanks in advance, and sorry for my English(still learning).
#edit
Thanks for quick reply.
For test purposes I created a button and text block:
<Button Click="spr" >Sprawdź</Button>
<TextBlock Text="{Binding Answers[0].IsSelected, Mode=TwoWay}" > </TextBlock>
It's in other controls part (above list box, but in FlipView)
Click method
private void spr(object sender, RoutedEventArgs e)
{
var ans = ((PrawoJazdyDataQuestion)this.flipView.SelectedItem).Answers;
foreach (var item in ans)
item.IsSelected = item.IsSelected ? false : true;
}
As I wrote, when i'm changing data from code side, it is changing content of element, but not appearance of ListBoxItem. And if I just select it on ListBox, it doesn't change the data in TextBlock neither in ListBox itself.
#edit2 fixing typos...
To change the IsSelected property of the ListBoxItem, you need to change the ListBox.ItemContainerStyle. See here:
<ListBox Grid.Row="1" Grid.ColumnSpan="3"
SelectionMode="Multiple" VerticalAlignment="Center"
ItemsSource="{Binding Answers}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding IsSelected}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Since the binding mode is TwoWay, selecting and deselecting the ListBoxItem's dynamically changes the content of the items to display either True or False.
Also notice how I changed the ListBox.ItemTemplate into a TextBlock instead of a ListBoxItem. The ItemTemplate defines the content of the ListBoxItem, so some type of content control is typically used. Below is the UI structure for the different layouts (this can be viewed using the WPF Tree Visualizer).
ListBoxItem as the ItemTemplate:
TextBlock as the ItemTemplate:
Edit
Also note that I removed the IValueConverter. Since your source and target properties are both bool, there is no need for a converter in this case. Try removing the converter references to see if that fixes the problem.
Do not bind to IsSelected on ListBoxItem. ListBox got SelectedItem or SelectedItems property, where You should pass item(s) to be selected, i.e. by binding.
Setting IsSelected property in model is not good idea. Better make SelectedItem notify property in ViewModel.

Button Template style when selected

I have a mvvm design pattern set up to do commands and workspaces so that a button is pressed and the proper workspace displays. The buttons have a template binding to pull in the proper image for the template based on the command. I have added another field to the command for a SelectedTemplateResource for when the button is selected out of the array of buttons. The DataTemplate is pulled into the mainwindow to display the buttons.
<DataTemplate x:Key="CommandsTemplate">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}" Template="{Utilities:BindableResource {Binding Path=TemplateResource}}">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Command}">
<Setter Property="Template">
<Setter.Value>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
Want I want to do is set the value of the setter to the SelectedTemplateResource like so in the Template of the button called TemplateResource. These are all handled through the mainwindowviewmodel to set the appropriate template per button and it works well but I'm not sure how to finish the selected state.
Also, I'm not entirely sure if I should be setting the datatrigger binding to the command path.
Can anybody help me with figuring out how to set the selected state of a button using the design mentioned above?
Cheers.
EDIT
I get the use of the togglebutton trigger however the binding is not working. Heres some more information as for some reason I keep getting key is null error in my app.xaml.cs
The ToggleButton is a datatemplate in a file called MainWindowResources.xaml (ResourceDictionary).
The MainWindow.xaml file pulls in this data template.
I have a bindableresource class to help with the static resource for the resource file.
public class BindableResource : StaticResourceExtension
{
#region Fields
private static readonly DependencyProperty dummyProperty;
#endregion
#region Properties
/// <summary>
/// Gets and sets my binding.
/// </summary>
public Binding MyBinding { get; set; }
#endregion
#region Constructor
/// <summary>
/// Static contruction of the dummy dependency property.
/// </summary>
static BindableResource()
{
dummyProperty = DependencyProperty.RegisterAttached("Dummy", typeof(string), typeof(DependencyObject), new UIPropertyMetadata(null));
}
/// <summary>
/// Constructor.
/// </summary>
public BindableResource()
{
}
/// <summary>
/// Constructor with binding to set.
/// </summary>
/// <param name="binding"></param>
public BindableResource(Binding binding)
{
MyBinding = binding;
}
#endregion
#region External Members
/// <summary>
/// Get the resource key to bind to the resource.
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
var targetObject = (FrameworkElement)target.TargetObject;
MyBinding.Source = targetObject.DataContext;
var DummyDO = new DependencyObject();
BindingOperations.SetBinding(DummyDO, dummyProperty, MyBinding);
ResourceKey = DummyDO.GetValue(dummyProperty);
return base.ProvideValue(serviceProvider);
}
#endregion
}
However this does not work for the triggers setter value.
<DataTemplate>
<ToggleButton Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}" Template="{Utilities:BindableResource {Binding Path=TemplateResource}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Template" Value="{Utilities:BindableResource {Binding Path=SelectedTemplateResource}}" />
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
</DataTemplate>
Any thoughts or ideas?
I'm not sure I've understood what you want exactly. You want to change the button's template if it's selected yes?
Firstly a standard button doesn't have a the notion of being "selected". You want a ToggleButton for that. That way you can trigger on its IsChecked property to set your template.
<DataTemplate x:Key="CommandsTemplate">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ToggleButton Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}" Template="{Utilities:BindableResource {Binding Path=TemplateResource}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Template" Value="{Utilities:BindableResource {Binding DataContext.SelectedTemplateResource}}">
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
After finding out this really is not possible to do in pure xaml I brought out the c# and create a custom control... this is very basic and can be improved on and I will have change a bit of it but ultimately a custom control solves the issue so that you can hit the click event from within the resource dictionary and change the template on the fly.
public class TabButton : Button
{
public static readonly DependencyProperty SelectedTemplateProperty =
DependencyProperty.Register("SelectedTemplate", typeof(ControlTemplate), typeof(TabButton));
public ControlTemplate SelectedTemplate
{
get { return base.GetValue(SelectedTemplateProperty) as ControlTemplate; }
set { base.SetValue(SelectedTemplateProperty, value); }
}
public TabButton()
{
this.Click += new RoutedEventHandler(TabButton_Click);
}
~TabButton()
{
}
public void TabButton_Click(object sender, RoutedEventArgs e)
{
ControlTemplate template = (ControlTemplate)this.FindResource("Environmental Template Selected");
(sender as TabButton).Template = template;
}
}
Cheers.

Categories