How to force ListBox to reload properties of ListBoxItems - wpf

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.

Related

WPF AutoCompleteBox dynamic sorting

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>

GUI/view doesn't notice about changes in the viewmodel. Who should notify?

I'm rather new to Silverlight and have a question about the notifying-mechanism. My solution is an MVVM-application stacked like this:
VIEW Contains a RadGridView bound to a collection in the viewmodel, the data is an entitycollection. The GridView's SelectedItem is bound to corresponding property in the viewmodel.
VIEWMODEL
Holds the properties below that the GridView is bound to and implements INotifyPropertyChanged.
•SelectList - an entitycollection that inherits ObservableCollection. When SelectList is set, it runs a notify-call.
•SelectedItem - an entity that also implements INotifyPropertyChanged for its own properties. When SelectedItem is set, it runs a notify-call.
My question is, who should make the notify-call so that the GridView knows value has changed? Occasionally, a property in the entity is set programmatically directly in the viewmodel. As for now, nothing is happening in the GUI although the properties gets the new values correctly.
Regards, Clas
-- UPDATE WITH CODE -------------------------
VIEW
<UserControl
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
x:Class="X.Y.Z.MonthReport.MonthReportView"
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:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot">
<telerik:RadGridView x:Name="MonthReportGrid"
Grid.Row="1"
ItemsSource="{Binding SelectList}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
AutoGenerateColumns="False">
<telerik:RadGridView.Columns>
<!-- The other columns have been cut out of this example -->
<telerik:GridViewDataColumn DataMemberBinding="{Binding curDate, Mode=TwoWay, TargetNullValue=''}" DataFormatString="{} {0:d}" Header="Avläst datum" UniqueName="curDate" IsVisible="True" IsReadOnly="False">
<telerik:GridViewDataColumn.CellEditTemplate>
<DataTemplate>
<telerik:RadDateTimePicker SelectedValue="{Binding curDate, Mode=TwoWay, TargetNullValue=''}" InputMode="DatePicker" DateTimeWatermarkContent="ÅÅÅÅ-MM-DD" />
</DataTemplate>
</telerik:GridViewDataColumn.CellEditTemplate>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn DataMemberBinding="{Binding curValue, Mode=TwoWay, TargetNullValue=''}" Header="Avläst värde" UniqueName="curValue" IsVisible="True" IsReadOnly="False" />
</telerik:RadGridView>
</Grid>
</UserControl>
VIEW .CS
using System;
using System.Collections.Generic;
using System.Windows.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Windows.Controls;
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.GridView;
namespace X.Y.Z.MonthReport
{
public partial class MonthReportView : UserControl, IMonthReportView
{
/// <summary>
/// ViewModel attached to the View
/// </summary>
public IMonthReportViewModel Model
{
get { return this.DataContext as IMonthReportViewModel; }
set { this.DataContext = value; }
}
public MonthReportView()
{
InitializeComponent();
this.MonthReportGrid.CellEditEnded += new EventHandler<GridViewCellEditEndedEventArgs>(MonthReportGrid_OnCellEditEnded);
}
public void MonthReportGrid_OnCellEditEnded(object sender, GridViewCellEditEndedEventArgs e)
{
if (e.Cell.Column.UniqueName == "curValue")
{
// ...
this.Model.SetAutomaticReadingDate();
}
if (e.Cell.Column.UniqueName == "curDate")
{
this.Model.UpdateAutomaticReadingDate();
}
}
}
}
VIEWMODEL
using System;
using Microsoft.Practices.Prism.Events;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Prism.Commands;
namespace X.Y.Z.MonthReport
{
public class MonthReportViewModel : ViewModel<IMonthReportView>, IMonthReportViewModel
{
private readonly IEventAggregator eventAggregator;
private readonly IMonthReportService dataService;
private readonly IMonthReportController dataController;
private DateTime? _newReadingDate;
public DateTime? NewReadingDate
{
get { return _newReadingDate; }
set { _newReadingDate = value; }
}
//Holds the selected entity
private MonthReportEntity _selectedItem;
public MonthReportEntity SelectedItem
{
get { return _selectedItem; }
set
{
if (_selectedItem != value)
{
_selectedItem = value;
//The INotifyPropertyChanged implementation inherited from ViewModel-base.
Notify(() => this.SelectedItem);
}
}
}
//The entitycollection
private MonthReportEntityCollection _selectList;
public MonthReportEntityCollection SelectList
{
get { return _selectList; }
set
{
if (_selectList != value)
{
_selectList = value;
//The INotifyPropertyChanged implementation inherited from ViewModel-base.
Notify(() => this.SelectList);
}
}
}
public MonthReportViewModel(IMonthReportView view,
IEventAggregator eventAggregator, IMonthReportService dataService, IMonthReportController dataController)
{
this.InitializeCommands();
this.eventAggregator = eventAggregator;
this.dataController = dataController;
this.dataService = dataService;
this.View = view;
this.View.Model = this;
dataService.onGetMonthReportComplete += new EventHandler<MonthReportEventArgs>(OnGetMonthReportComplete);
dataService.onSaveMonthReportComplete += new EventHandler<MonthReportEventArgs>(OnSaveMonthReportComplete);
InitializeData();
}
public void InitializeCommands()
{
// ...
}
public void InitializeData()
{
GetMonthReport();
}
//This function is not working as I want it to.
//The gridview doesn't notice the new value.
//If a user edits the grid row, he should not need to
//add the date manually, Therefor I use this code snippet.
public void SetAutomaticReadingDate()
{
if ((NewReadingDate.HasValue) && (!SelectedItem.curDate.HasValue))
{
SelectedItem.curDate = NewReadingDate;
//The INotifyPropertyChanged implementation inherited from ViewModel-base.
Notify(() => this.SelectedItem.curDate);
}
}
public void GetMonthReport()
{
dataService.GetMonthReport();
}
public void SaveMonthReport()
{
dataService.SaveMonthReport(SelectList);
}
void OnGetMonthReportComplete(object sender, MonthReportEventArgs e)
{
// ...
}
void OnSaveMonthReportComplete(object sender, MonthReportEventArgs e)
{
// ...
}
#region ICleanable
public override void Clean()
{
base.Clean();
}
#endregion
}
}
if you do your binding like this
<telerik:GridViewDataColumn DataMemberBinding="{Binding curValue, Mode=TwoWay, TargetNullValue=''}" Header="Avläst värde" UniqueName="curValue" IsVisible="True" IsReadOnly="False" />
you just have to look at the binding to know where you have to call PropertyChanged and your binding said:
the class whith the property "curValue" has to implement INotifyProperyChanged to get the View informed.
public void SetAutomaticReadingDate()
{
if ((NewReadingDate.HasValue) && (!SelectedItem.curDate.HasValue))
{
//this is enough if the class of SelectedItem implements INotifyPropertyChanged
//and the curDate Poperty raise the event
SelectedItem.curDate = NewReadingDate;
}
}
btw BAD code style to name the Property curDate! it should be CurDate, Properties with camlCase hurts my eyes :)
Your "MonthReportEntityCollection" must implement interface "INotifyCollectionChanged" to allow informing UI about collection changes (items add/remove).
Your "MonthReportEntity" must implement interface "INotifyPropertyChanged" to allow informing UI about entitie's property changing.
Other stuff looks correct.

Set ComboBox SelectedItem to dataobject not present in ItemSource

I have a combobox whose SelectedItem and ItemSource properties are bound to the viewmodel. The objects in the ItemSource are time sensitive in that the objects expire and I can only set active items in the collection to the ItemSource. Now at a certain point, the selectedItem object maynot be in the ItemSource.
I think the normal behaviour of the ItemSource is to only allow objects to be selected from the ItemSource Collection but in this case I do want to select an object which is no longer in the ItemSource.
How can I implement this behaviour? I will post some code here to support my problem.
Window.xaml
<Window x:Class="ExpiredSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ComboBox Height="23"
HorizontalAlignment="Left"
Margin="184,68,0,0"
Name="comboBox1"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding CustomList}"
SelectedItem="{Binding ActiveItem}"
SelectedValue="Code"
SelectedValuePath="{Binding ActiveItem.Code}"
DisplayMemberPath="Code"
/>
</Grid>
Window.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ExpiredSelectedItem
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CustomList = new List<DomainObject>();
CustomList.AddRange(new DomainObject[] {
new DomainObject() { Code = "A", IsActive =true},
new DomainObject() { Code ="B", IsActive = true},
new DomainObject() { Code = "C", IsActive =true},
});
ActiveItem = new DomainObject() { Code = "D", IsActive = false };
this.DataContext = this;
}
public List<DomainObject> CustomList { get; set; }
public DomainObject ActiveItem { get; set; }
}
public class DomainObject
{
public string Code { get; set; }
public bool IsActive { get; set; }
}
}
Even though I select the code D in the code-behind, when the combobox loads the first item (A) is selected. Expected behaviour was that "D" should have been selected in the textbox but no items should be when the dropdown is opened.
I don't know if this plays any role, but it sure looks suspicious. Try removing it:
<ComboBox ... SelectedValue="Code" ...
To keep the selected item when it is not in the list you can to do the following:
Prevent the ActiveItem from getting set to NULL; see: if (value != null)
Clear the ComboBox; see: comboBox1.SelectedIndex = -1;
Implement INotifyPropertyChanged (which you probably already do)
These changes might cause your code more harm than good, but hopefully this gets you started.
Here is the code-behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace ComboBoxSelectedItem
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void Notify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public MainWindow()
{
InitializeComponent();
CustomList = new List<DomainObject>();
CustomList.AddRange(new DomainObject[] {
new DomainObject() { Code = "A", IsActive =true},
new DomainObject() { Code ="B", IsActive = true},
new DomainObject() { Code = "C", IsActive =true},
});
this.DataContext = this;
}
public List<DomainObject> CustomList { get; set; }
private DomainObject _activeItem = new DomainObject() { Code = "D", IsActive = false };
public DomainObject ActiveItem
{
get { return _activeItem; }
set
{
if (value != null)
{
_activeItem = value;
}
Notify("ActiveItem");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ActiveItem = new DomainObject() { Code = "D", IsActive = false };
comboBox1.SelectedIndex = -1;
}
}
}
Here is the XAML:
<Window x:Class="ComboBoxSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
Height="23"
HorizontalAlignment="Left"
Name="comboBox1"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding CustomList}"
SelectedItem="{Binding ActiveItem}"
DisplayMemberPath="Code"/>
<TextBox
Grid.Row="1"
Text="{Binding Path=ActiveItem.Code, Mode=TwoWay}"/>
<Button
Grid.Row="2"
Content="D"
Click="Button_Click"/>
</Grid>
</Window>

Strange behaviour when binding to ListBox in WPF

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.

DataContextChanged of a Tab in a TabControl is raised too early

I have a TabControl binding to some items. Underneath it is a Button where I can add items dynamically. On adding an item, the new item should become the active Tab (works fine with TabControl.SelectedItem):
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TabControl ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=OneWay}">
<TabControl.ContentTemplate>
<DataTemplate>
<this:UserControl1 />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<Button Content="Foo" Click="Button_Click"/>
</StackPanel>
</Window>
Code-behind:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : INotifyPropertyChanged
{
public ObservableCollection<Foo> Items { get; set; }
public Foo SelectedItem { get { return Items.Last(); } }
public event PropertyChangedEventHandler PropertyChanged;
public MainWindow()
{
Items = new ObservableCollection<Foo>();
Items.Add(new Foo {Bar = "bar"});
InitializeComponent();
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Items.Add(new Foo {Bar = "bar"});
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Items"));
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
}
public class Foo { public string Bar { get; set; } }
}
The UserControl1 looks like this:
<UserControl x:Class="WpfApplication1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBox/>
<TextBox x:Name="_textBox"
DataContextChanged="OnDataContextChanged"
Text="{Binding Bar}" />
</StackPanel>
</UserControl>
And the code-behind of it should focus _textBox and selectAll its text when the user clicks on the tab:
using System.Windows;
namespace WpfApplication1
{
public partial class UserControl1
{
public UserControl1()
{
InitializeComponent();
}
private void OnDataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
_textBox.Focus();
_textBox.SelectAll();
}
}
}
I try to achieve that with the DataContextChanged-event, but due to its unpredictability (s.f. http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.datacontextchanged.aspx), it doesn't work all the time. I also tried it with the Loaded-event, but this will be called only once when the DataTemplate is loaded.
So, I think I need to receive the Loaded-event every time the DataContext has changed and the data-binding engine has finished its job. Is there such an event?
Are you wanting to select the text when the user adds a tab AND when the user clicks on a different tab?
If this is the case you may want to to handle this with two event handlers - The tab changed event for the tab control - and then setting it in code when you add a new item.
The DataContext according to your code does not change. It is set to the main window and then inherited down to the child controls.
public MainWindow()
{
Items = new ObservableCollection<Foo>();
Items.Add(new Foo {Bar = "bar"});
InitializeComponent();
DataContext = this;
}

Resources