WPF derived ComboBox SelectedValuePath issue - wpf

In our application we have a very large set of data that acts as our data dictionary for ComboBox lists, etc. This data is staticly cached and keyed off of 2 variables, so I thought it wise to write a control that derived from ComboBox and exposed the 2 keys as DPs. When those 2 keys have proper values I set the ItemsSource of the ComboBox automatically from the data dictionary list it corresponds to. I also automatically set the SelectedValuePath and DisplayMemberPath in the constructor to Code and Description, respectively.
Here's how an example of how an item in the ItemsSource from the data dictionary list always looks:
public class DataDictionaryItem
{
public string Code { get; set; }
public string Description { get; set; }
public string Code3 { get { return this.Code.Substring(0, 3); } }
}
The value of Code is always 4 characters long, but sometimes I only need to bind 3 characters of it. Hence, the Code3 property.
Here's how the code looks inside my custom combobox to set the ItemsSource:
private static void SetItemsSource(CustomComboBox combo)
{
if (string.IsNullOrEmpty(combo.Key1) || string.IsNullOrEmpty(combo.Key2))
{
combo.ItemsSource = null;
return;
}
List<DataDictionaryItem> list = GetDataDictionaryList(combo.Key1, combo.Key2);
combo.ItemsSource = list;
}
Now, my problem is, when I change the SelectedValuePath in the XAML to Code3, it doesn't work. What I bind to SelectedValue still gets the full 4 character Code from DataDictionaryItem. I even tried rerunning SetItemsSource when the SelectedValuePath was changed and no dice.
Can anyone see what I need to do to get my custom combobox to wake up and use the SelectedValuePath provided if it's overridden in the XAML? Tweaking the value in the property setter in my SelectedValue bound business object is not an option.
Here's how the XAML looks for my combobox in a form:
<c:CustomComboBox Key1="0" Key2="8099" SelectedValuePath="Code3" SelectedValue="{Binding Thing}"/>
EDIT: I just ran snoop on my code and it says my SelectedValuePath is Code... it doesn't appear to ever be set to Code3... Zuh?

Ok, I figured it out.
Apparently setting the default values of a DependencyProperty in the default non-static constructor of a WPF control is a no-no. So, at first I tried this:
static ValueCodeListComboBox()
{
SelectedValuePathProperty.OverrideMetadata(typeof(ValueCodeListComboBox), new PropertyMetadata("Code"));
DisplayMemberPathProperty.OverrideMetadata(typeof(ValueCodeListComboBox), new PropertyMetadata("Description"));
}
But this kept throwing an error saying:
Metadata override and base metadata must be of the same type or derived type.
Finally figured out that meant I needed to use FrameworkPropertyMetadata instead of PropertyMetadata:
static ValueCodeListComboBox()
{
SelectedValuePathProperty.OverrideMetadata(typeof(ValueCodeListComboBox), new FrameworkPropertyMetadata("Code"));
DisplayMemberPathProperty.OverrideMetadata(typeof(ValueCodeListComboBox), new FrameworkPropertyMetadata("Description"));
}
Now changing SelectedValuePath in the XAML works great.

Related

How to pass parameter to ComboBox ItemsSource binding?

I am working with WPF for the first time, so please bear with me.
I have a combobox, which is meant to generically display some lookup data. The models for the different types of lookups are exactly the same, just different data sources which are retrieved via a single method call passing different enumerations to control the returned data set. Fairly simple stuff.
public sealed class MyDataProvider
{
public enum Types
{
Green,
Blue,
Orange
}
private readonly ConcurrentDictionary<string, ObservableCollection<LookUpVm>> _lookupData =
new ConcurrentDictionary<string, ObservableCollection<LookUpVm>>();
private static readonly Lazy<MyDataProvider> lazy =
new Lazy<MyDataProvider>(() => new MyDataProvider());
public static MyDataProvider Instance => lazy.Value;
private MyDataProvider()
{
}
public ObservableCollection<LookUpVm> GreenLookupDataSource => GetLookupDataSource(Types.Green);
public ObservableCollection<LookUpVm> GetLookupDataSource(Types lookupEnum)
{
ObservableCollection<LookUpVm> lookupDataSource;
if (_lookupData.TryGetValue(lookupEnum, out lookupDataSource))
return lookupDataSource;
lookupDataSource = new ObservableCollection<LookUpVm>();
var returnedlookupDataSource =
SomeMasterSource.GetlookupDataSourceBylookupEnum(lookupEnum).OrderBy(ia => ia.Name);
foreach (var returnedLookupData in returnedlookupDataSource)
{
lookupDataSource.Add(returnedLookupData);
}
_lookupData.TryAdd(lookupEnum, lookupDataSource);
return lookupDataSource;
}
}
This works great for the 0th iteration, where I create a GreenLookupComboBox.
<ComboBox ItemsSource="{Binding Source={x:Static objectDataProviders:MyDataProvider.Instance},
Path=GreenLookupDataSource}" />
However, what I really need to be able to do is to set up a combobox which can have its Types enum value set on the parent View, which would then call directly to the GetLookupDataSource and pass the enum. We have several dozen lookup types, and defining a new property for each feels less than ideal. Something like the below for the control view...
<ComboBox ItemsSource="{Binding Source={x:Static objectDataProviders:MyDataProvider.Instance},
Path=GetLookupDataSource}" />
And something like the below for where I use the lookup control.
<local:MyLookupControl Type=Types.Green />
Is this even possible?
EDIT:
Here's an example of what I'm trying to accomplish.
I have two key-value pairs of lists.
ListOne
1 - A
2 - B
3 - C
and
ListTwo
1 - X
2 - Y
3 - Z
They are accessible by calling the method GetList(Enum.LookupType). They share the same ViewModel and View. However, I need to place both of them on a form for my users to select from.
I'm looking for some way to use XAML like the following on the View they appear on.
<local:MyLookupControl Method=GetList Parameter=Enum.ListOne/>
<local:MyLookupControl Method=GetList Parameter=Enum.ListTwo />
This should display a pair of comboboxes, one bound to ListOne and one bound to ListTwo.
You're essentially just trying to set up databinding on a couple controls. This is simple then so long as you have the correct datacontext for the view.
Controls can be bound to properties (which is exactly what you are looking for).
Using your edited example here is how you would do that:
private ObservableCollection<string> _listOne;
private ObservableCollection<string> _listTwo;
private string _selectedListOneItem;
private string _selectedListTwoItem;
public ObservableCollection<string> ListOne
{
get { return _listOne; }
set { _listOne = value; }
}
public ObservableCollection<string> ListTwo
{
get { return _listTwo; }
set { _listTwo = value; }
}
public string SelectedListOneItem
{
get { return _selectedListOneItem; }
set { _selectedListOneItem = value; }
}
public string SelectedListTwoItem
{
get { return _selectedListTwoItem; }
set { _selectedListTwoItem = value; }
}
XAML:
<ComboBox ItemsSource="{Binding ListOne}" SelectedItem="{Binding SelectedListOneItem}"/>
<ComboBox ItemsSource="{Binding ListTwo}" SelectedItem="{Binding SelectedListTwoItem}"/>
You have several options in how you want to load or get your lists. You can either load them in the constructor or for something a bit heavier is load them every time you "get" in the property. I would recommend loading those in the constructor.
What I provided is basically autoprops and can even be further simplified but I wanted to show you that you can also write code in the getter and setter of those properties to further expand on the items. For instance, you may want something on the background to fire off when SelectedListOneItem changes. In this case on the SET of SelectedListOneItem you can set the value but then also run a method/function which may update other properties.
WPF is very dependent on properties to bind between ViewModels and Views. In your response before the edit you are using fields which can't be bound to controls in a view.
EDIT:
If you do plan on updating properties in the ViewModel that would change things on the view you are also going to want to look into INotifyPropertyChanged. By implementing INotifyPropertyChanged the view will be updated/notified when properties are changing. INotifyPropertyChanged comes with it's own event that you must invoke in the setting of a property. Here is also a very helpful method you can call that will fire this event for you much easier.
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
You call this like so:
public string SelectedListOneItem
{
get { return _selectedListOneItem; }
set
{
_selectedListOneItem = value;
OnPropertyChanged();
}
}
That way if anything else in the ViewModel updates SelectedListOneItem that your view will make the appropriate change. In this case it would make the combobox select the new value you set in SelectedListOneItem.

Extended WPF Toolkit - CheckComboBox

Is anyone aware of a way to manually enable (turning on the tick) on the Check Boxes within the CheckComboBox for WPFToolkit?
Unfortunately, the Items in the Combo-box are all strings.
I'm trying to enable all flags when "Select All" checkbox is ticked.
This is a rather late response but I thought it best to post this in case it helps someone out. I have used the following approach for the WPFToolkit version:
public class Descriptor : INotifyPropertyChanged
{
private bool isSelected;
public bool IsSelected
{
get
{
return this.isSelected;
}
set
{
if (this.isSelected != value)
{
this.isSelected = value;
// Raise INotifyPropertyChanged
}
}
}
public string Name { get; set; }
}
Create a collection of these and then assign them to the ItemsSource of the CheckComboBox.
To handle select all we have an option labelled: "" as the first item in the collection, then if this item is ticked all the items are de-selected and the all case is handle under the hood. To handle the selection Changed it does involve adding an event to the Descriptor class and firing it each time the IsSelected property is changed.
I eventually tossed out Extended WPFToolkit due to it's inability to access the checkboxes directly.
Instead I created a ComboBox and manually defined Checkboxes within it, which I access directly by name, and there able to implement a "Select All" by using it's [Checked/Unchecked[ event, and use the ComboBox SelectionChanged to show a default value that expresses what has been selected in a CSV format.
Maybe be clunky, but it gets the job done.
PS. I did not need to even bother with a DataTemplate for the ComboBox
One way in the code Behind is
var ComboSelector = MyCheckComboBox as Xceed.Wpf.Toolkit.Primitives.Selector;
foreach(var item in MyCheckComboBox.Items)
ComboSelector.SelectedItems.Add(item);

Where the combobox bound items are coming from?

May be it's a silly (or more than trivial) kinda question, but it seems i just don't know the answer. Here's the case -
I assigned a UserList as the ItemsSource of a combobox. So what i did essentially is assigning a reference type to another.
I cleared the UserList. So now i get the Count of the ItemsSource 0 as well.
I still get the items present in my combobox. And i also can cast the SelectedItem of the combobox to a User object.
Here's the complete code -
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public partial class MainWindow : Window
{
private List<User> _userList;
public MainWindow()
{
InitializeComponent();
_userList = new List<User>()
{
new User() {Id = 1, Name = "X"},
new User() {Id = 2, Name = "Y"},
new User() {Id = 3, Name = "Z"}
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.comboBox1.ItemsSource = _userList;
this.comboBox1.DisplayMemberPath = "Name";
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_userList.Clear();
/* ItemsSource is cleared as well*/
IEnumerable userList = this.comboBox1.ItemsSource;
/*I can still get my User*/
User user = this.comboBox1.SelectedItem as User;
}
}
So, where the items are coming from? What actually happens under-the-hood when i make such binding? Does the control have some kind of cache? It's a royal pain to realize not having such basic ideas. Can anybody explain the behind-the-scene detail?
EDIT : I wrote the code in WPF, but i have the same question for WinForms Combobox.
EDIT : Doesn't a combobox display its items from it's in-memory Datasource? When that datasource contains 0 items, how does it display the items?
When you set an ItemsSource of any ItemsControl it copies the ref to the list into its Items property. Then it subscribes to the OnCollectionChanged event, and creates a CollectionView object. So, on the screen you can see that collectionView.
as I have found in source code ItemCollection holds two lists:
internal void SetItemsSource(IEnumerable value)
{
//checks are missed
this._itemsSource = value;
this.SetCollectionView(CollectionViewSource.GetDefaultCollectionView((object) this._itemsSource, this.ModelParent));
}
How could you get SelectedItem?
This is my assumption from quick look into the source code:
ItemsControl has a collection of "views" and each View sholud store a ref to the item (User instance), because it has to draw data on the screen. So, when you call SelectedItem it returns a saved ref.
Upd about references
Assume there is an User instance. It has the adress 123 in memory. There is a list. It stores references. One of them is 123.
When you set an ItemsSource ItemsControl saves a reference to the list, and creates a Views collection. Each view stores a references to an item. One view stores an address 123.
Then you cleared a list of users. Now list doesn't contains any references to Users. But in memory there is an adrress 123 and there is an instance of User by this adress. Garbage Collector doesn't destroy it, because View has a reference to it.
When you get SelectedItem it returns User instance from the 123 adress.
var user = new User();
var list = new List<User>();
list.Add(user);
list.Clear();
Console.WriteLine(list.Count()); //prints 0 - list is empty
Console.WriteLine(user == null); //prints false. - user instance is sill exists;
In answer to your comment to #GazTheDestroyer ("... why it doesn't get cleared, and how it holds the items?")
In WPF, when you set the ItemsSource property of an ItemsControl, the control will wrap the list of items in a CollectionView, which is a collection type optimised for use by the UI framework. This CollectionView is assigned to the Items property of the control and is what the display-drawing code actually works from. As you see, this collection is entirely separate of the object you originally assigned to ItemsSource, and so there is no propogation of changes from one to the other. This is why the items are still in the control when you clear the original list: the control is ignoring the original list, and has its own list that contains your objects.
It's for this reason that an ItemsSource value needs to raise events - specifically INotifyCollectionChanged.NotifyCollectionChanged - so that the control knows to refresh the Items list. ObservableCollection implements this interface and raises the correct event, and so the functionality works as expected.
It's hugely important to note that this is nothing like what happens in WinForms, which is why I've been pressing you for the clarification.
EDIT: To clarify, there is no "deep copy." The code that is happening is similar in principle to the following:
private List<object> myCopy;
public void SetItemsSource(List<object> yourCopy)
{
myCopy = new List<object>();
foreach (var o in yourCopy)
{
myCopy.Add(o);
}
}
Once this code has run, there's only one copy of every item in your list. But each of the items is in both of the lists. If you change, clear or otherwise manipulate yourCopy, myCopy knows nothing about it. You cannot "destroy" any of the objects that are within the list my clearing yourCopy - all you do is release your own reference to them.
Assuming you are using WPF:
List<User> doesn't fire any event that the UI will recognise to refresh itself. If you use ObservableCollection<User> instead, your code will work.
The key difference is that ObservableCollection implements INotifyCollectionChanged, which allows the UI to recognise that the content of the collection has changed, and thus refresh the content of the ComboBox.
(Note that this does not work in WinForms. In WinForms you can set the DataSource property of the control, but the same ObservableCollection trick does not work here.)
When you set a collection reference to ItemsControl, all the combo gets is a reference, that it knows is enumerable.
It will enumerate the reference and display the items. Whether it does a deep copy or shallow copy is irrelevant, all it has is a reference (memory address effectively).
If you change your collection in some way, the combo has no way of knowing unless you tell it somehow. The reference (address) hasn't changed, everything looks the same to the combo. You seem to be thinking that the object is somehow "live" and the combo can watch the memory changing or something? This isn't the case. All it has is a reference that it can enumerate over. The contents can change but without some trigger the combo doesn't know that, and so will sit doing nothing.
ObservableCollection is designed to overcome this. It implements INotifyCollectionChanged that fires events when it changes, so the Combo knows that it must update its display.

ComboBox Binding Collection and Selected Property

Why doesn't #2 work? (It seems like most examples say to do this).
#1(works)<ComboBox ItemsSource="{Binding Marker.ReadOnlyContentRegions}"
SelectedItem="{Binding Marker.SelectedRegion}"
SelectedValue="{Binding
Marker.SelectedRegion.UniqueId, Mode=TwoWay}"
SelectedValuePath="UniqueId"
DisplayMemberPath="Label" />
#2(doesn't work)<ComboBox ItemsSource="{Binding Marker.ReadOnlyContentRegions}"
SelectedValue="{Binding Marker.SelectedRegion.UniqueId,Mode=TwoWay}"
SelectedValuePath="UniqueId"
DisplayMemberPath="Label" />
This is the class that contains the objects that should be databound.
class...
public CancellableObservableCollection<InvisibleContentMarkerBase>
ReadOnlyContentRegions
{
get { return
CancellableObservableCollection<InvisibleContentMarkerBase>)
GetValue(ReadOnlyContentRegionsProperty); }
set { SetValue(ReadOnlyContentRegionsProperty, value); }
}
public static readonly DependencyProperty ReadOnlyContentRegionsProperty =
DependencyProperty.Register("ReadOnlyContentRegions",
typeof(CancellableObservableCollection<InvisibleContentMarkerBase>),
typeof(TargetedContentMarker), new UIPropertyMetadata(null);
public InvisibleContentMarkerBase SelectedRegion
{
get { return (InvisibleContentMarkerBase)GetValue(SelectedRegionProperty); }
set { SetValue(SelectedRegionProperty, value); }
}
public static readonly DependencyProperty SelectedRegionProperty =
DependencyProperty.Register("SelectedRegion",
typeof(InvisibleContentMarkerBase), typeof(TargetedContentMarker), new
UIPropertyMetadata(null));
...// end of class
First of all, don't set both the SelectedItem and the SelectedValue
They both set the exact same property, so when you set both only one value will actually get used
When you set SelectedValuePath and SelectedValue, you are setting the selected item by value. The SelectedValuePath tells WPF what property on objects in the collection is the Id field, and SelectedValue tells WPF to set the selected item to the value that is equal to SelectedValue.
SelectedItem simply tells WPF to select the item in the collection that exactly matches the SelectedItem object. Note that this comparison is by reference, so if the SelectedItem is a class that doesn't point to the exact same reference in memory as one of the objects in the ItemsSource, it won't evaluate the two objects as the same and won't set the item as Selected
So in short, either get rid of the SelectedItem binding and just use SelectedValue/SelectedValuePath, or remove SelectedValue/SelectedValuePath and ensure that the object bound in SelectedItem refers to the exact same object in memory as the copy in the ItemsSource.
If you really can't reference that object, and insist on using SelectedItem instead of SelectedValue, you could also overwrite the .Equals() on your class so it returns true if the data is equal, regardless of if the memory reference is the same. I prefer to avoid this since this changes the functionality of any instances of this class, but wanted to let you know that option is available.

Silverlight: how to bind List<T> to data grid

MVVM pattern is implemented in my Silverlight4 application.
Originally, I worked with ObservableCollection of objects in my ViewModel:
public class SquadViewModel : ViewModelBase<ISquadModel>
{
public SquadViewModel(...) : base(...)
{
SquadPlayers = new ObservableCollection<SquadPlayerViewModel>();
...
_model.DataReceivedEvent += _model_DataReceivedEvent;
_model.RequestData(...);
}
private void _model_DataReceivedEvent(ObservableCollection<TeamPlayerData> allReadyPlayers, ...)
{
foreach (TeamPlayerData tpd in allReadyPlayers)
{
SquadPlayerViewModel sp = new SquadPlayerViewModel(...);
SquadPlayers.Add(sp);
}
}
...
}
Here is a peacie of XAML code for grid displaying:
xmlns:DataControls="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Data"
...
<DataControls:DataGrid ItemsSource="{Binding SquadPlayers}">
...</DataControls:DataGrid>
and my ViewModel is bound to DataContext property of the view.
This collection (SquadPlayers) is not changed after its creation so I would like to change its type to
List<SquadPlayerViewModel>
. When I did that, I also added
RaisePropertyChanged("SquadPlayers")
in the end of '_model_DataReceivedEvent' method (to notify the grid that list data are changed.
The problem is that on initial displaying grid doesn't show any record... Only when I click on any column header it will do 'sorting' and display all items from the list...
Question1: Why datagrid doesn't contain items initially?
Q2: How to make them displayed automatically?
Thanks.
P.S. Here is a declaration of the new List object in my view-model:
public List<SquadPlayerViewModel> SquadPlayers { get; set; }
You can't use List as a binding source, because List not implement INotifyCollectionChanged it is require for WPF/Silverlight to have knowledge for whether the content of collection is change or not. WPF/Sivlerlight than can take further action.
I don't know why you need List<> on your view model, but If for abstraction reason you can use IList<> instead. but make sure you put instance of ObservableCollection<> on it, not the List<>. No matter what Type you used in your ViewModel Binding Only care about runtime type.
so your code should like this:
//Your declaration
public IList<SquadPlayerViewModel> SquadPlayers { get; set; }
//in your implementation for WPF/Silverlight you should do
SquadPlayers = new ObservableCollection<SquadPlayerViewModel>();
//but for other reason (for non WPF binding) you can do
SquadPlayers = new List<SquadPlayerViewModel>();
I usually used this approach to abstract my "Proxied" Domain Model that returned by NHibernate.
You'll need to have your SquadPlayers List defined something like this:
private ObservableCollection<SquadPlayerViewModel> _SquadPlayers;
public ObservableCollection<SquadPlayerViewModel> SquadPlayers
{
get
{
return _SquadPlayers;
}
set
{
if (_SquadPlayers== value)
{
return;
}
_SquadPlayers= value;
// Update bindings, no broadcast
RaisePropertyChanged("SquadPlayers");
}
}
The problem is that whilst the PropertyChanged event informs the binding of a "change" the value hasn't actually changed, the collection object is still the same object. Some controls save themselves some percieved unnecessary work if they believe the value hasn't really changed.
Try creating a new instance of the ObservableCollection and assigning to the property. In that case the currently assigned object will differ from the new one you create when data is available.

Resources