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
}
}
Related
I have a combobox and two buttons on the same page. The buttons are "Previous" and "Next". Ideally I'd like to use the buttons to select combobox items accordingly. Also, I'd like "Next" and "Previous" buttons to be disabled when end or beginning of the list is reached. I don't think I would have a problem with doing this in code-behind but I wonder if this is possible through xaml/binding/mvvm. Thanks.
I would suggest the following approach:
Regular Combobox
Buttons (Previous and Next) with Enabled property binded to ComboBox SelectedIndex using a Converter
You can then set the new Index using regular button Events or thorugh Commands
Yes it's perfectly possible. Firstly, I would strongly recommend that you use an MVVM framework, secondly using a framework such as Caliburn.Micro, you can easily invoke verbs (methods) on your view model from Buttons in your view.
So, for example, you would have a <Button x:Name="Next">Next</Button> in your view, and a corresponding public void Next() method in your view model. Caliburn.Micro will take care of the convention based binding from the Button click event to the view model's method with the same name.
In your Next method, you would increment the current page. This would also be a public property on your view model, which implements INotifyPropertyChanged. An MVVM framework such as Caliburn.Micro provides a method which will invoke the PropertyChanged event using a lambda expression, rather than a magic property name string.
Then, in your view, you would bind the ComboBox ItemsSource to a collection of possible page numbers, and the SelectedItem to the current page public property.
As it sounds like you're doing paging, you might also want to consider the PagedList library (UI agnostic) and corresponding NuGet package.
Quick and Dirty Implementation .
cs :
public partial class MainWindow : Window , INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private List<string> _items;
public List<string> Items
{
get
{
if (_items == null)
_items = new List<string> { "1", "2", "3", "4", "5" };
return _items;
}
}
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
if(PropertyChanged!= null)
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
private ICommand _nextCommand;
public ICommand NextCommand
{
get
{
if (_nextCommand == null)
_nextCommand = new RelayCommand<string>(MoveToNext, CanMoveToNext);
return _nextCommand;
}
}
private ICommand _previousCommand;
public ICommand PreviousCommand
{
get
{
if (_previousCommand == null)
_previousCommand = new RelayCommand<string>(MoveToPrevious, CanMoveToPrevious);
return _previousCommand;
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void MoveToNext(object currentItem)
{
var currentIndex = _items.IndexOf((string)currentItem);
SelectedItem = _items.ElementAt(currentIndex + 1);
}
private bool CanMoveToNext(object currentItem)
{
return currentItem != null && _items.Last<string>() != (string)currentItem;
}
private void MoveToPrevious(object currentItem)
{
var currentIndex = _items.IndexOf((string)currentItem);
SelectedItem = _items.ElementAt(currentIndex - 1);
}
private bool CanMoveToPrevious(object currentItem)
{
return currentItem != null && _items.First<string>() != (string)currentItem;
}
}
RelayCommand
XAML :
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="Next" Command="{Binding NextCommand}" CommandParameter="{Binding ElementName=box, Path=SelectedItem}"/>
<Button Content="Prev" Grid.Column="1" Command="{Binding PreviousCommand}" CommandParameter="{Binding ElementName=box, Path=SelectedItem}"/>
<ComboBox x:Name="box"
Grid.Row="1" Grid.ColumnSpan="2"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
/>
</Grid>
I am trying to grasp the concepts of WPF data binding through a simple example, but it seems I haven't quite gotten the point of all of it.
The example is one of cascading dropdowns; the XAML is as follows:
<Window x:Class="CascadingDropDown.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="496" Width="949" Loaded="Window_Loaded">
<Grid>
<ComboBox Name="comboBox1" ItemsSource="{Binding}" DisplayMemberPath="Key" SelectionChanged="comboBox1_SelectionChanged" />
<ComboBox Name="comboBox2" ItemsSource="{Binding}" DisplayMemberPath="Name" />
</Grid>
</Window>
This is the code of the form:
public partial class MainWindow : Window
{
private ObservableCollection<ItemA> m_lstItemAContext = new ObservableCollection<ItemA>();
private ObservableCollection<ItemB> m_lstItemBContext = new ObservableCollection<ItemB>();
private IEnumerable<ItemB> m_lstAllItemB = null;
public MainWindow()
{
InitializeComponent();
this.comboBox1.DataContext = m_lstItemAContext;
this.comboBox2.DataContext = m_lstItemBContext;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var lstItemA = new List<ItemA>() { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
var lstItemB = new List<ItemB>() { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
initPicklists(lstItemA, lstItemB);
}
private void initPicklists(IEnumerable<ItemA> lstItemA, IEnumerable<ItemB> lstItemB)
{
this.m_lstAllItemB = lstItemB;
this.m_lstItemAContext.Clear();
lstItemA.ToList().ForEach(a => this.m_lstItemAContext.Add(a));
}
#region Control event handlers
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox ddlSender = (ComboBox)sender;
ItemA itemaSelected = (ItemA)ddlSender.SelectedItem;
var lstNewItemB = this.m_lstAllItemB.Where(b => b.KeyA == itemaSelected.Key);
this.m_lstItemBContext.Clear();
lstNewItemB.ToList().ForEach(b => this.m_lstItemBContext.Add(b));
}
private void comboBox2_?(object sender, ?EventArgs e)
{
// disable ComboBox if empty
}
#endregion Control event handlers
}
And these are my data classes:
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
}
class ItemB
{
public string KeyA { get; set; }
public string Name { get; set; }
public ItemB(string sKeyA, string sName)
{
this.KeyA = sKeyA;
this.Name = sName;
}
}
So whenever an item is selected in comboBox1, the appropriate items are supposed to show up in comboBox2. This is working with the current code, though I'm not sure whether my way of re-populating the respective ObservableCollection is ideal.
What I haven't been able to achieve is actually reacting to changes in the underlying collection of comboBox2, for example to deactivate the control when the list is empty (i.e. when "ccc" is selected in comboBox1).
Of course, I can use an event handler on the CollectionChanged event of the ObservableCollection, and that would work in this example, but in a more complex scenario, where the ComboBox' DataContext might change to a completely different object (and possibly back), that would mean a two-fold dependency - I would always have to not only switch the DataContext, but also the event handlers back and forth. This doesn't seem right to me, but I am probably simply on an entirely wrong track about this.
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items".
What do I need to do, or where do I have to correct my perception of the whole concept ?
Here is the cleaner (perhaps not the much optimized) way to acheive this, keeping your business model untouched, and using ViewModel and XAML only when possible :
View Model :
public class WindowViewModel : INotifyPropertyChanged
{
private ItemA selectedItem;
private readonly ObservableCollection<ItemA> itemsA = new ObservableCollection<ItemA>();
private readonly ObservableCollection<ItemB> itemsB = new ObservableCollection<ItemB>();
private readonly List<ItemB> internalItemsBList = new List<ItemB>();
public WindowViewModel()
{
itemsA = new ObservableCollection<ItemA> { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
InvokePropertyChanged(new PropertyChangedEventArgs("ItemsA"));
internalItemsBList = new List<ItemB> { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
}
public ObservableCollection<ItemA> ItemsA
{
get { return itemsA; }
}
public ItemA SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
ItemsB.Clear();
var tmp = internalItemsBList.Where(b => b.KeyA == selectedItem.Key);
foreach (var itemB in tmp)
{
ItemsB.Add(itemB);
}
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
}
}
public ObservableCollection<ItemB> ItemsB
{
get { return itemsB; }
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
Code Behind :
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new WindowViewModel();
}
}
and XAML :
<StackPanel>
<ComboBox Name="comboBox1" ItemsSource="{Binding ItemsA}" DisplayMemberPath="Key" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
<ComboBox Name="comboBox2" ItemsSource="{Binding ItemsB}" DisplayMemberPath="Name">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="IsEnabled" Value="true"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsB.Count}" Value="0">
<Setter Property="IsEnabled" Value="false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</StackPanel>
copying-pasting this should work.
Few random thoughts :
1) in WPF, try to always use MVVM pattern and never put code in code-behind files for event handlers. For user actions (like button clicks) use the Commands pattern. For other user actions for which commands are not available, think as much as you can in a "binding-way" : you can do a lot since you can intercept event from the view in VM properties setters (in your example I use the SelectedItem property setter).
2) Use XAML as much as you can. WPF framework provides a very powerful binding and triggers system (in your example, the enabling of combobox don't needs any line of C#).
3) ObservableCollection are made to be exposed by the view model to the view via binding. They are also meant to be used in conjunction with their CollectionChanged event that you can handle in the view model. Take benefit of that (in your example, I play with Observable collection in the VM, where this playing should happen, and any changes in the collection gets reflected in the view via DataBinding).
Hopes this will help !
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items"
if you wanna use MVVM pattern then i would say NO. not the control should give the information, but your viewmodel should.
taking an ObservableCollection is a good step at first. in your specail case i would consider to create just one list with ItemA and i would add a new List property of type ItemB to ItemA.
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
public IEnumerable<ItemB> ListItemsB { get; set;}
}
i assume ItemA is the parent?
class ItemB
{
public string Name { get; set; }
public ItemB(string sName)
{
this.Name = sName;
}
}
you have a collection of ItemA and each ItemA has its own list of depending ItemB.
<ComboBox x:Name="cbo_itemA" ItemsSource="{Binding ListItemA}" DisplayMemberPath="Key"/>
<ComboBox ItemsSource="{Binding ElementName=cbo_itemA, Path=SelectedItem.ListItemsB}"
DisplayMemberPath="Name" />
Do you need the Keys collection? If not i'd suggest creating it dynamically from the items by grouping via CollectionView:
private ObservableCollection<object> _Items = new ObservableCollection<object>()
{
new { Key = "a", Name = "Item 1" },
new { Key = "a", Name = "Item 2" },
new { Key = "b", Name = "Item 3" },
new { Key = "c", Name = "Item 4" },
};
public ObservableCollection<object> Items { get { return _Items; } }
<StackPanel>
<StackPanel.Resources>
<CollectionViewSource x:Key="ItemsSource" Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Key"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</StackPanel.Resources>
<StackPanel.Children>
<ComboBox Name="keyCb" ItemsSource="{Binding Source={StaticResource ItemsSource}, Path=Groups}" DisplayMemberPath="Name"/>
<ComboBox ItemsSource="{Binding ElementName=keyCb, Path=SelectedItem.Items}" DisplayMemberPath="Name"/>
</StackPanel.Children>
</StackPanel>
The first ComboBox shows the keys which are generated by grouping by the Key-property, the second binds to the selected item's subitems in the first ComboBox, showing the Name of the item.
Also see the CollectionViewGroup reference, in the fist CB i use the Name in the second the Items.
Of course you can create these key-groups manually as well by nesting items in a key-object.
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.
I have a datagrid which contains bid and ask - prices for currency-symbols. The data is updated every seconds. I update the data by creating a new collection of my viewmodel-entities and bind this collection to the datagrid every second.
The problem is:
Because my datagrid contains a template-column with a button "buy", this button is also recreated every second! This means, that when the user hovers the button, the hover-styles blinks, as the button is recreated every second. Additionally sometimes the click-event is not fired correctly, if the button is recreated while the user has his mouse-left-button pressed.
Any suggestions, how to solve real-time-update a datagrid with button-columns?
If I understand correctly, you have a collection of items and you have a couple fields (bid/ask in particular), all of which will be updated every second. It sounds like what may be happening is that in the process of changing the ItemsSource of your data grid, you're losing some important state that is causing problems for the event handlers on your buttons.
Even if you update all of the items, the important distinction to make is that you should update the items, and not completely clear out the collection that is currently bound to your datagrid. Changing the ItemsSource to a new one will cause the data grid to have to do a lot more work than if you simply update the contents of the existing collection. If you're using an ObservableCollection, this may mean making your viewmodel items mutable so that you can simply update bid/ask. If your viewmodel items are mutable and implement INotifyPropertyChanged, the bid/ask updates will be reflected in the datagrid or in any other bindings to those properties of the objects. The neat thing about doing it this way is that the same objects are staying bound to the same containers in the ItemsControl, so during each update, absolutely nothing is happening to your buttons. Now, if your viewmodel objects that contain bid/ask are immutable, you should still be able to pull this off. Every second, you simply iterate through your collection of items and use SetItem to replace each existing item with a new one. The important thing to remember in this latter case is that every second, the datagrid is still getting notified that there has been a change in the ObservableCollection, and because of this, the bindings on each row are going to cause the DataContext of the row/cells/button to update.
Here's a quick example of how I might go about this problem. I'm going to assume use of the datagrid in .NET 4.0 (if you're using toolkit though with 3.5, this should be the same). I'm going to take the first approach, where my CurrencyPair object is mutable.
First, some simple viewmodel code with a self contained timer to update a few currency pair bid/asks each second:
public class CurrencyPairsViewModel
{
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
private readonly ObservableCollection<string> _orders = new ObservableCollection<string>();
private readonly ObservableCollection<CurrencyPair> _pairs = new ObservableCollection<CurrencyPair>();
private readonly Random _rand = new Random();
private readonly System.Timers.Timer _timer = new System.Timers.Timer(1000);
private readonly Action _update;
public CurrencyPairsViewModel()
{
this._timer.Elapsed += OnIntervalElapsed;
this._update = new Action(this.Update);
this._pairs.Add(new CurrencyPair("USD/GBP"));
this._pairs.Add(new CurrencyPair("AUD/USD"));
this._pairs.Add(new CurrencyPair("WOW/CAD"));
this._timer.Start();
}
public ObservableCollection<string> Orders { get { return this._orders; } }
public ObservableCollection<CurrencyPair> Pairs { get { return this._pairs; } }
public void Buy(CurrencyPair pair)
{
this._orders.Add(string.Format("Buy {0} at {1}", pair.Name, pair.Ask));
}
private void OnIntervalElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
this._dispatcher.Invoke(this._update);
}
private void Update()
{
foreach (var pair in this._pairs)
{
pair.Bid = this._rand.NextDouble();
pair.Ask = pair.Bid + 0.01;
}
this._timer.Start();
}
}
public class CurrencyPair : INotifyPropertyChanged
{
private readonly string _name;
private double _ask;
private double _bid;
public CurrencyPair(string name)
{
this._name = name;
}
public event PropertyChangedEventHandler PropertyChanged;
public double Ask
{
get { return this._ask; }
set
{
this._ask = value;
this.OnPropertyChanged("Ask");
}
}
public double Bid
{
get { return this._bid; }
set
{
this._bid = value;
this.OnPropertyChanged("Bid");
}
}
public string Name { get { return this._name; } }
protected void OnPropertyChanged(string name)
{
if (null != this.PropertyChanged)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
Second, the view, which in this example is just my MainWindow.
<Window x:Class="WpfApplication1.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 Height="*"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
ItemsSource="{Binding Pairs}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="Bid" Binding="{Binding Bid}" Width="*"/>
<DataGridTextColumn Header="Ask" Binding="{Binding Ask}" Width="*"/>
<DataGridTemplateColumn Header="Buy">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="BUY"
Click="OnBuyClicked"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<ListBox Grid.Row="2"
ItemsSource="{Binding Orders}"/>
</Grid>
</Window>
And finally, I have a bit of code behind this XAML to handle the BUY button clicks and initialize a viewmodel right in the view (note that this, and other practices outside of how to update the bid/ask on the collection of items may not be the best way to go about things, depending on how your application is going to grow).
public partial class MainWindow : Window
{
private readonly CurrencyPairsViewModel _model = new CurrencyPairsViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = this._model;
}
private void OnBuyClicked(object sender, RoutedEventArgs e)
{
var pair = (CurrencyPair)((Button)sender).DataContext;
this._model.Buy(pair);
}
}
Hope the example is helpful!
Have you looked into the ObservableCollection?
Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
This should only refresh those items that are changed, rather than the whole grid.
Consider the following XAML:
<ComboBox Name="CompanyComboBox"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Path=GlobalData.Companies}"
SelectedValuePath="Id"
SelectedValue="{Binding Customer.CompanyId, ValidatesOnDataErrors=True}"
DisplayMemberPath="Name" />
GlobalData.Companies is a collection (IEnumerable<Company>) of companies; this collection can be reloaded on background (it is downloaded from a webservice). When this happens, ComboBox correctly reloads items via binding. However as a side-effect, it also resets the selected item!
I have used Reflector to inspect combo-box sources and apparently this is intended behavior.
Is there any "nice" way how to get around this? What I want to achieve, is that if the user selects "Company A" and reloads list of companies afterwards, then "Company A" stays selected (assuming it is in the new list).
Please try with the following code.
Enable the following property to the combo box
IsSynchronizedWithCurrentItem="True"
Maybe you can use ObservableCollection<Company> instead of your IEnumerable<Company>? Then, on background change you would only Add / Remove items that are new / absent in the new list, selected item should stay, unless it was removed by the change.
You can update your observable collection in a separate thread with a small hack-around.
hmm, I don't know if it is a "nice" way, but if you can access the selected item before the reload occurs, you can save it (or its key or something), and select it programatically again after the reload is done.
quick mockup:
var selectedItem = myCombo.SelectedItem;
DoReload();
myCombo.SelectedItem = selectedItem;
But I assume you mean another way than this manual work around?
Hope this helps anyway...
UPDATE
Ok I see, from a background thread.
Are you using an ICollectionView to bind your combobox too? If so, you can use the CurrentItem property to keep a reference. I made a quick mockup, and this is working on my setup. this assumes you have a reference to your UI:
XAML
<Grid VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox ItemsSource="{Binding Items}" IsSynchronizedWithCurrentItem="True" Grid.Column="0" Grid.Row="0" DisplayMemberPath="Name"/>
<Button Command="{Binding UpdateCommand}" Grid.Column="1" Grid.Row="0">Update</Button>
</Grid>
View/ViewModel
public partial class Window1 : Window {
public Window1() {
InitializeComponent();
this.DataContext = new ViewModel(this);
}
}
public class ViewModel
{
private readonly Window1 window;
private ObservableCollection<Item> items;
private ICollectionView view;
public ViewModel(Window1 window) {
this.window = window;
items = new ObservableCollection<Item>
{
new Item("qwerty"),
new Item("hello"),
new Item("world"),
};
view = CollectionViewSource.GetDefaultView(items);
}
public ObservableCollection<Item> Items { get { return items; } }
public ICommand UpdateCommand {
get { return new RelayCommand(DoUpdate); }
}
public Item SelectedItem { get; set; }
private void DoUpdate(object obj) {
var act = new Func<List<Item>>(DoUpdateAsync);
act.BeginInvoke(CallBack, act);
}
private List<Item> DoUpdateAsync() {
return new List<Item> {
new Item("hello"),
new Item("world"),
new Item("qwerty"),
};
}
private void CallBack(IAsyncResult result) {
try {
var act = (Func<List<Item>>)result.AsyncState;
var list = act.EndInvoke(result);
window.Dispatcher.Invoke(new Action<List<Item>>(delegate(List<Item> lst) {
var current = lst.Single(i => i.Name == ((Item)view.CurrentItem).Name);
Items.Clear();
lst.ForEach(Items.Add);
view.MoveCurrentTo(current);
}), list);
} catch(Exception exc){ Debug.WriteLine(exc); }
}
}
public class Item {
public Item(string name) {
Name = name;
}
public string Name { get; set; }
}
You will need to do some handling in case the selected item is no longer in the list.
The IsSynchronizedWithCurrentItem property is important here, else it won't work!
Also, the way the reference to the main window is made should be by a DI-framework.
As Yacoder pointed out this has to do with object equality. As long as you bind SelectedValue instead of SelectedItem you can define the ItemsSource as an anonymous type collection. Then this problem will not occur (and it is also faster if you need to read the values from a database).