I noticed some strange behaviour when binding an array to a ListBox. When I add items with the same "name", I can't select them in runtime - the ListBox goes crazy. If I give them unique "names", it works just fine. Could anyone please explain WHY is this happening?
The View:
<Window x:Class="ListBoxTest.ListBoxTestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ListBoxTest"
Title="ListBoxTestView" Height="300" Width="300">
<Window.Resources>
<local:ListBoxTestViewModel x:Key="Model" />
</Window.Resources>
<Grid DataContext="{StaticResource ResourceKey=Model}">
<ListBox ItemsSource="{Binding Items}" Margin="0,0,0,70" />
<Button Command="{Binding Path=Add}" Content="Add" Margin="74,208,78,24" />
</Grid>
</Window>
The View Model:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Input;
namespace ListBoxTest
{
internal class ListBoxTestViewModel : INotifyPropertyChanged
{
private List<string> realItems = new List<string>();
public ListBoxTestViewModel()
{
realItems.Add("Item A");
realItems.Add("Item B");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public string[] Items
{
get { return realItems.ToArray(); }
}
public ICommand Add
{
// DelegateCommand from Prism
get { return new DelegateCommand(DoAdd); }
}
private int x = 1;
public void DoAdd()
{
var newItem = "Item";
// Uncomment to fix
//newItem += " " + (x++).ToString();
realItems.Add(newItem);
OnPropertyChanged("Items");
}
}
}
All items in a WPF ListBox must be unique instances. Strings that have the same constant value are not unique instances due to string Interning. To get around this, you need to encapsulate the item in a more meaningful object than a String, such as:
public class DataItem
{
public string Text { get; set; }
}
Now you can instantiate multiple DataItem instances and create an ItemDataTemplate to render the Text as a TextBlock. You can also override the DataItem ToString() if you want to use the default rendering. You can now have multiple DataItem instances with the same Text and no problems.
This limitation may seem a bit strange, but it simplifies the logic, as now SelectedItem has a one-to-one correspondence with SelectedIndex for items in the list. It is also in line with the WPF approach to data visualization, which tends toward lists of meaningful objects as opposed to lists of plain strings.
Related
I am trying to work out wpf with some difficulties. This ComboBox seems a very basic issue but I can't have it populated even after reading all possible similar post.
The extra difficulty I think is that the ComboBox is defined in a resource, here is the resource code:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:DiagramDesigner">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/Shared.xaml"/>
<ResourceDictionary Source="Styles/ToolBar.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ToolBar x:Key="MyToolbar" Height="120">
<!--Languages-->
<GroupBox Header="Localization" Style="{StaticResource ToolbarGroup}" Margin="3">
<Grid>
<ComboBox Height="23" HorizontalAlignment="Center"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding _langListString}"
DisplayMemberPath="ValueString"
SelectedValuePath="ValueString"
SelectedValue="{Binding LangString}"
/>
</Grid>
</GroupBox>
</ToolBar>
My data object is defined as follow:
public partial class Window1 : Window
{
List<ComboBoxItemString> _langListString = new List<ComboBoxItemString>();
// Object to bind the combobox selections to.
private ViewModelString _viewModelString = new ViewModelString();
public Window1()
{
// Localization settings
_langListString.Add(new ComboBoxItemString()); _langListString[0].ValueString = "en-GB";
_langListString.Add(new ComboBoxItemString()); _langListString[1].ValueString = "fr-FR";
_langListString.Add(new ComboBoxItemString()); _langListString[2].ValueString = "en-US";
// Set the data context for this window.
DataContext = _viewModelString;
InitializeComponent();
}
And the modelview:
/// This class provides us with an object to fill a ComboBox with
/// that can be bound to string fields in the binding object.
public class ComboBoxItemString
{
public string ValueString { get; set; }
}
//______________________________________________________________________
//______________________________________________________________________
//______________________________________________________________________
/// Class used to bind the combobox selections to. Must implement
/// INotifyPropertyChanged in order to get the data binding to
/// work correctly.
public class ViewModelString : INotifyPropertyChanged
{
/// Need a void constructor in order to use as an object element
/// in the XAML.
public ViewModelString()
{
}
private string _langString = "en-GB";
/// String property used in binding examples.
public string LangString
{
get { return _langString; }
set
{
if (_langString != value)
{
_langString = value;
NotifyPropertyChanged("LangString");
}
}
}
#region INotifyPropertyChanged Members
/// Need to implement this interface in order to get data binding
/// to work properly.
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
I just don't know what to try else. Is anyone has an idea of what is going on, and why the combobox stays empty?
Many thanks.
you can just bind to public properties
ItemsSource="{Binding _langListString}"
can not work because _langListString is not a public property
By my analysis the problem consist in your DataContext.
DataContext = _viewModelString;
If you give the viewModelString to the DataContext you have to have the _langListString >defined there, in order to the combobox know which item it is bound to.
This is what I would do:
Add List _langListString = new List(); to the >ModelView.
_langListString would be _viewModelString._langListString.add(Your Items) - be >carefull to instatiate the _langList when you create your _viewModelString object.
Then I think the rest would work.
Many thanks, I have the changes you've suggested but this combobox still stays empty :-(
The new modelview looks like this:
/// Class used to bind the combobox selections to. Must implement
/// INotifyPropertyChanged in order to get the data binding to
/// work correctly.
public class ViewModelString : INotifyPropertyChanged
{
public List<ComboBoxItemString> _langListString {get;set;}
/// Need a void constructor in order to use as an object element
/// in the XAML.
public ViewModelString()
{
// Localization settings
_langListString = new List<ComboBoxItemString>();
ComboBoxItemString c;
c = new ComboBoxItemString(); c.ValueString = "en-GB"; _langListString.Add(c);
c = new ComboBoxItemString(); c.ValueString = "fr-FR"; _langListString.Add(c);
c = new ComboBoxItemString(); c.ValueString = "en-US"; _langListString.Add(c);
}
private string _langString = "en-GB";
/// String property used in binding examples.
public string LangString
{
get { return _langString; }
set
{
if (_langString != value)
{
_langString = value;
NotifyPropertyChanged("LangString");
}
}
}
#region INotifyPropertyChanged Members
/// Need to implement this interface in order to get data binding
/// to work properly.
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The data object:
// Object to bind the combobox selections to.
private ViewModelString _viewModelString;
public Window1()
{
// Set the data context for this window.
_viewModelString = new ViewModelString();
DataContext = _viewModelString;
InitializeComponent();
}
And I have tried all possible combination in the combobox (_langListString, _viewModelString._langListString, _viewModelString) it just doesn't work:
<ComboBox Height="23" HorizontalAlignment="Center"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding _langListString}"
DisplayMemberPath="ValueString"
SelectedValuePath="ValueString"
SelectedValue="{Binding LangString}"
/>
I tend to think that this xaml is making things really complicated without possibility of debugging. Is anyone can help???
I am using the WPF AutoCompleteBox and I have it working great, but one thing I would like to do is sort the suggestion list on the fly after each letter is entered into the primary TextBox. Does anyone know how to do this? I tried using an ICollectionView property with the DefaultView logic and adding SortDescriptions but it doesn't seem to phase the suggestion list. To make sure my collection view sorting was working I put a normal ListBox control and an AutoCompleteBox control on the same window and bound both controls to the same observable collection with the same collection view and the normal ListBox control showed the items sorted correctly using the SortDescriptions, but the AutoCompleteBox list didn't have the items sorted. It had them in the order they were added to the collection.
Thoughts? Suggestions? Has anyone done this?
I have no idea how #user1089031 done this, but here is working sample for anyone who could be interested in (updated to #adabyron's comment!):
ViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;
namespace WpfApplication12
{
public class Item
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}
public class ViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate {};
private readonly ObservableCollection<Item> source;
private readonly ICollectionView items;
private string searchText;
public ViewModel()
{
source = new ObservableCollection<Item>
{
new Item {Name = "111111111 Test abb - (1)"},
new Item {Name = "22222 Test - (2)"},
new Item {Name = "333 Test - (3)"},
new Item {Name = "44444 Test abc - (4)"},
new Item {Name = "555555 Test cde - (5)"},
new Item {Name = "66 Test - bbcd (6)"},
new Item {Name = "7 Test - cd (7)"},
new Item {Name = "Test - ab (8)"},
};
items = new ListCollectionView(source);
}
public ICollectionView Items
{
get { return items; }
}
public IEnumerable<Item> ItemsSorted
{
get
{
return string.IsNullOrEmpty(SearchText)
? source
: (IEnumerable<Item>)source
.OrderBy(item => item.Name.IndexOf(SearchText,
StringComparison.InvariantCultureIgnoreCase));
}
}
public Item Selected { get; set; }
public string SearchText
{
get { return searchText; }
set
{
searchText = value;
PropertyChanged(this,
new PropertyChangedEventArgs("SearchText"));
PropertyChanged(this,
new PropertyChangedEventArgs("ItemsSorted"));
}
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"
xmlns:wpfApplication2="clr-namespace:WpfApplication12"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="MainWindow" Height="200" Width="500"
DataContext="{DynamicResource viewModel}">
<Window.Resources>
<wpfApplication2:ViewModel x:Key="viewModel" />
<DataTemplate DataType="{x:Type wpfApplication2:Item}">
<TextBlock Text="{Binding Name}" FontFamily="Courier New" />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<controls:AutoCompleteBox
ItemsSource="{Binding ItemsSorted}"
FilterMode="ContainsOrdinal"
SelectedItem="{Binding Selected, Mode=TwoWay}"
MinimumPrefixLength="0"
VerticalAlignment="Top" Margin="5">
<i:Interaction.Behaviors>
<wpfApplication2:SearchTextBindBehavior
BoundSearchText="{Binding SearchText,
Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
</controls:AutoCompleteBox>
<ListBox Grid.Column="1"
ItemsSource="{Binding Items}" Margin="5" />
</Grid>
</Window>
As you could notice I've add one custom behavior to AutoCompleteBox control:
<i:Interaction.Behaviors>
<wpfApplication2:SearchTextBindBehavior
BoundSearchText="{Binding SearchText,
Mode=OneWayToSource}" />
</i:Interaction.Behaviors>
This is because AutoCompleteBox's own SearchText property is read-only. So here is the code of this behavior:
SearchTextBindBehavior.cs (Updated)
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApplication12
{
public class SearchTextBindBehavior : Behavior<AutoCompleteBox>
{
public static readonly DependencyProperty BoundSearchTextProperty =
DependencyProperty.Register("BoundSearchText",
typeof(string), typeof(SearchTextBindBehavior));
public string BoundSearchText
{
get { return (string)GetValue(BoundSearchTextProperty); }
set { SetValue(BoundSearchTextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.TextChanged -= OnTextChanged;
}
private void OnTextChanged(object sender, RoutedEventArgs args)
{
if(AssociatedObject.Text.Length == 0)
{
BoundSearchText = string.Empty;
return;
}
if(AssociatedObject.SearchText ==
AssociatedObject.Text.Substring(0,
AssociatedObject.Text.Length - 1))
{
BoundSearchText = AssociatedObject.Text;
}
}
}
}
Note: To make it all work you will need to add reference to the System.Windows.Interactivity.dll from the Expression Blend 4 SDK. This is just where Behavior<T> and a few its friends live.
If you have Expression Blend already installed, you already have all the SDK and there is no need to download anything. Just in case - on my machine the assembly located here:
C:\Program Files\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll
And, finally, if you have some good reason to do NOT add reference to this popular official library, feel free to re-implemented this custom behavior in "the old way" via plain old attached properties.
Hope that helps.
This is what I ended up with, a slight adaptation of Sevenate's answer, so if you wanted to upvote, do that to his post.
I used a subclass (I had the AutoCompleteBox subclassed already for other reasons), which allows me to create a wrapper dependency property to get the readonly SearchText (=what the user entered via keyboard) to the ViewModel - instead of a blend behavior, which is a perfectly valid way, too.
The crux of the matter is that you should only apply the dynamic sorting upon changes of SearchText, not Text (=what is displayed in the AutoCompleteBox, will also change if a suggestion is selected in the dropdown). Sevenate's way to raise the PropertyChanged event of the readonly ItemsSource (ItemsSorted) is a nice and clean way to apply the sorting.
ViewModel:
public class Item
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}
public class AutoCompleteBoxDynamicSortingVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly ObservableCollection<Item> source;
public AutoCompleteBoxDynamicSortingVM()
{
source = new ObservableCollection<Item>
{
new Item {Name = "111111111 Test abb - (1)"},
new Item {Name = "22222 Test - (2)"},
new Item {Name = "333 Test - (3)"},
new Item {Name = "44444 Test abc - (4)"},
new Item {Name = "555555 Test cde - (5)"},
new Item {Name = "66 Test - bbcd (6)"},
new Item {Name = "7 Test - cd (7)"},
new Item {Name = "Test - ab (8)"},
};
}
public IEnumerable<Item> ItemsSorted
{
get
{
return string.IsNullOrEmpty(Text) ? (IEnumerable<Item>)source :
source.OrderBy(item => item.Name.IndexOf(Text, StringComparison.OrdinalIgnoreCase));
}
}
public Item Selected { get; set; }
// Text that is shown in AutoCompleteBox
private string text;
public string Text
{
get { return text; }
set { text = value; OnPropertyChanged("Text"); }
}
// Text that was entered by user (cannot be changed from viewmodel)
private string searchText;
public string SearchText
{
get { return searchText; }
set
{
searchText = value;
OnPropertyChanged("SearchText");
OnPropertyChanged("ItemsSorted");
}
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Subclass of AutoCompleteBox:
public class MyAutoCompleteBox : AutoCompleteBox
{
/// <summary>
/// Bindable property that encapsulates the readonly property SearchText.
/// When the viewmodel tries to set SearchText by way of EnteredText, it will fail without an exception.
/// </summary>
public string EnteredText
{
get { return (string)GetValue(EnteredTextProperty); }
set { SetValue(EnteredTextProperty, value); }
}
public static readonly DependencyProperty EnteredTextProperty = DependencyProperty.Register("EnteredText", typeof(string), typeof(MyAutoCompleteBox), new PropertyMetadata(null));
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// synchronize SearchText and EnteredText (only one-way)
if (e.Property == AutoCompleteBox.SearchTextProperty && this.EnteredText != this.SearchText)
EnteredText = SearchText;
base.OnPropertyChanged(e);
}
}
Xaml:
<UserControl x:Class="WpfApplication1.Controls.AutoCompleteBoxDynamicSorting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myctrls="clr-namespace:WpfApplication1.Controls"
xmlns:models="clr-namespace:WpfApplication1.ViewModels"
Height="350" Width="525"
DataContext="{DynamicResource viewModel}">
<UserControl.Resources>
<models:AutoCompleteBoxDynamicSortingVM x:Key="viewModel" />
<DataTemplate DataType="{x:Type models:Item}">
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</UserControl.Resources>
<Grid>
<myctrls:MyAutoCompleteBox
ItemsSource="{Binding ItemsSorted}"
Text="{Binding Text, Mode=TwoWay}"
EnteredText="{Binding SearchText, Mode=OneWayToSource}"
FilterMode="ContainsOrdinal"
VerticalAlignment="Top" Margin="5" />
</Grid>
</UserControl>
I've defined a ListView where each item is displayed with a read-only (i.e. selection-only, no TextBox) ComboBox (as per the ItemTemplate).
There is a given list of possible items that can be chosen from each of these combo boxes. The caveat, however, is that no item may be selected in two combo boxes at a time. In order to ensure this, once an item has been selected in one of the combo boxes, it must be removed from all other combo boxes (obviously except for the one where it is selected), and once it gets deselected, it must be added to all other combo boxes again.
Oh, and one more thing: The order of the visible items must not change compared to the complete list.
My question is: How can I achieve this behavior?
I have tried the three possible solutions that came to my mind:
I have written a new helper control class that takes the complete list of existing items and a list of excluded (used) items via bindings to the outside world, as well as a property for a selected item. I could include that control class in the ItemTemplate; the combo box within that template would then bind its ItemsSource property to an ItemsProvider property of the helper class that was supposed to tie together the list of existing items, the list of excluded items and the selected item and return a single enumeration of items for that particular combo box.
However, I somehow got lost in all the update notifications about list changes; I fear I'd have to individually react to all combinations of NotifyCollectionChangedAction of the two input lists, and with the prospect of having a dozen update methods, I thought this cannot be the right way.
I changed the list of existing items to a list that stores a boolean along with each item, so I could flag each item as hidden or not. This would relieve me of the necessity to have a list of excluded items while maintaining the order of items, thereby reducing the aforementioned complexity of combined change notifications.
Unfortunately, as the list itself doesn't change with that solution, I don't know how to have the dependency property infrastructure notify my ItemsSource property in my helper class.
I don't have to use the WPF with bindings way; I can do code-behind here, too. So, I tried iterating over all ListViewItems and retrieving the combo box for each of them to manually refresh the item lists. However, I couldn't find a good time to access the ListViewItems after their item template had been loaded. There seems to be no event for that situation, and ListView.ItemContainerGenerator is read-only, so even if ItemContainerGenerator were not a sealed class, I couldn't assign my own specialized ItemContainerGenerator that would create custom list view items where I could override OnApplyTemplate.
I would probably bind all ComboBoxes to different CollectionViews over the source collection which filter out the selected items of the other ComboBoxes. You also need to Refresh the views if the selections of the combo-boxes change.
If you bind the Lists to different Lists in the ViewModel, and bind the selected item to trigger a method that changes those Lists, then you could get your result. Similar to the below.
Xaml of MainWindow.xaml:
<Window x:Class="ComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="26" />
</Grid.RowDefinitions>
<ComboBox Name="cboOne" Grid.Column="0" Grid.Row="0" ItemsSource="{Binding CboOneList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboOneValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboTwo" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding CboTwoList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboTwoValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboThree" Grid.Column="2" Grid.Row="0" ItemsSource="{Binding CboThreeList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboThreeValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
</Grid>
</Window>
Code-behind for MainWindow.xaml:
using System.Windows;
using System.Windows.Controls;
namespace ComboBox {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.DataContext = new ComboBoxViewModel();
}
private void cboOne_SelectionChanged(object sender, SelectionChangedEventArgs e) {
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace ComboBox {
class ComboBoxViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
List<string> master = new List<string>() { "A", "B", "C", "D", "E", "F" };
#region C'tor
public ComboBoxViewModel() {
RetrieveLists();
}
#endregion
#region Methods
protected void OnPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = this.PropertyChanged;
if(null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void RetrieveLists() {
List<string> tempOne = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboOneList = tempOne;
List<string> tempTwo = (from a in master
where !a.Equals(CboOneValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboTwoList = tempTwo;
List<string> tempThree = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboOneValue)
select a).ToList();
CboThreeList = tempThree;
}
#endregion
#region Properties
private string cboOneValue = string.Empty;
public string CboOneValue {
get {
return cboOneValue;
}
set {
if(!value.Equals(cboOneValue)) {
cboOneValue = value;
RetrieveLists();
OnPropertyChanged("CboOneValue");
}
}
}
private string cboTwoValue = string.Empty;
public string CboTwoValue {
get {
return cboTwoValue;
}
set {
if(!value.Equals(cboTwoValue)) {
cboTwoValue = value;
RetrieveLists();
OnPropertyChanged("CboTwoValue");
}
}
}
private string cboThreeValue = string.Empty;
public string CboThreeValue {
get {
return cboThreeValue;
}
set {
if(!value.Equals(cboThreeValue)) {
cboThreeValue = value;
RetrieveLists();
OnPropertyChanged("CboThreeValue");
}
}
}
private List<string> cboOneList = new List<string>();
public List<string> CboOneList {
get {
return cboOneList;
}
set {
cboOneList = value;
OnPropertyChanged("CboOneList");
}
}
private List<string> cboTwoList = new List<string>();
public List<string> CboTwoList {
get {
return cboTwoList;
}
set {
cboTwoList = value;
OnPropertyChanged("CboTwoList");
}
}
private List<string> cboThreeList = new List<string>();
public List<string> CboThreeList {
get {
return cboThreeList;
}
set {
cboThreeList = value;
OnPropertyChanged("CboThreeList");
}
}
#endregion
}
}
I want to compare two versions of various properties and bold one of them if it is not equal to the other. Since SL4 doesn't support MultiBinding I am binding the FontWeight to "." so that the entire data context is passed to the converter. I then use the converter parameter to specify which fields to compare within the converter. So far, so good... Values that don't match are bolded.
The problem is that the bolded property is bound to a text box which can be edited. When the value is edited, I want the converter to be "re-activated" so that the font weight is set according to the new value. This doesn't happen. How can this be accomplished?
Note: I have already implemented INotifyPropertyChanged for the relevant class and properties. Tabbing to the next field after changing the value causes the PropertyChanged event to fire, but the font weight is not updated until I actually move to a different record and then return to the record that was changed.
(I also tried using Mode=TwoWay to see if that would do the trick. However, TwoWay binding cannot be used when you are binding to ".")
Do you NEED to use a Value Converter? I tried this quick using the MVVM pattern and it worked pretty well. If you could use MVVM, you could possibly do it like this:
MainPage.xaml
<UserControl x:Class="BindBoldText.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BindBoldText"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<local:MainPage_ViewModel/>
</UserControl.DataContext>
<StackPanel>
<TextBlock Text="{Binding Value1, Mode=TwoWay}"/>
<TextBlock Text="{Binding Value2, Mode=TwoWay}" FontWeight="{Binding Value2FontWeight}"/>
<TextBox Text="{Binding Value2, Mode=TwoWay}" TextChanged="TextBox_TextChanged"/>
</StackPanel>
MainPage.xaml.cs
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.viewModel = this.DataContext as MainPage_ViewModel;
}
private MainPage_ViewModel viewModel;
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
viewModel.Value2 = (sender as TextBox).Text;
}
}
MainPage_ViewModel.cs
public class MainPage_ViewModel : INotifyPropertyChanged
{
public string Value1
{
get { return value1; }
set
{
if (value1 != value)
{
value1 = value;
OnPropertyChanged("Value1");
}
}
}
private string value1 = "Test";
public string Value2
{
get { return value2; }
set
{
if (value2 != value)
{
value2 = value;
OnPropertyChanged("Value2");
OnPropertyChanged("Value2FontWeight");
}
}
}
private string value2 = "Test";
public FontWeight Value2FontWeight
{
get
{
if (value2.Equals(value1))
{
return FontWeights.Normal;
}
else
{
return FontWeights.Bold;
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
In your solution, the value does not get updated because the property itself is bound against the whole data context via the "." expression. INotifyPropertyChanged may be called, but this event means that a single property has changed, and since you don't provide a property name in the binding expression, the data binding system does not know that the Binding needs to be updated - it can't look into what your value converter does.
I think JSprang's appraoch is a lot better, not at least because it provides better separation of presentation logic that can be tested from the markup. To go further with a clean interface, you could let the ViewModel implement a boolean property "ValuesAreSame", data-bind against it, and use a value converter to apply the actual visual style (in this case, a font weight).
Is there any way, how to force ObservableCollection to fire CollectionChanged?
I have a ObservableCollection of objects ListBox item source, so every time I add/remove item to collection, ListBox changes accordingly, but when I change properties of some objects in collection, ListBox still renders the old values.
Even if I do modify some properties and then add/remove object to the collection, nothing happens, I still see old values.
Is there any other way around to do this? I found interface INotifyPropertyChanged, but I don't know how to use it.
I agree with Matt's comments above. Here's a small piece of code to show how to implement the INotifyPropertyChanged.
===========
Code-behind
===========
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Windows.Documents;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
Nicknames names;
public Window1()
{
InitializeComponent();
this.addButton.Click += addButton_Click;
this.names = new Nicknames();
dockPanel.DataContext = this.names;
}
void addButton_Click(object sender, RoutedEventArgs e)
{
this.names.Add(new Nickname(myName.Text, myNick.Text));
}
}
public class Nicknames : System.Collections.ObjectModel.ObservableCollection<Nickname> { }
public class Nickname : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
void Notify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propName));
}
}
string name;
public string Name
{
get { return name; }
set
{
name = value;
Notify("Name");
}
}
string nick;
public string Nick
{
get { return nick; }
set
{
nick = value;
Notify("Nick");
}
}
public Nickname() : this("name", "nick") { }
public Nickname(string name, string nick)
{
this.name = name;
this.nick = nick;
}
public override string ToString()
{
return Name.ToString() + " " + Nick.ToString();
}
}
}
XAML
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<DockPanel x:Name="dockPanel">
<TextBlock DockPanel.Dock="Top">
<TextBlock VerticalAlignment="Center">Name: </TextBlock>
<TextBox Text="{Binding Path=Name}" Name="myName" />
<TextBlock VerticalAlignment="Center">Nick: </TextBlock>
<TextBox Text="{Binding Path=Nick}" Name="myNick" />
</TextBlock>
<Button DockPanel.Dock="Bottom" x:Name="addButton">Add</Button>
<ListBox ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" />
</DockPanel>
</Grid>
Modifying properties on the items in your collection won't fire NotifyCollectionChanged on the collection itself - it hasn't changed the collection, after all.
You're on the right track with INotifyPropertyChanged. You'll need to implement that interface on the class that your list contains. So if your collection is ObservableCollection<Foo>, make sure your Foo class implements INotifyPropertyChanged.