I'm trying to display a Wpf Treeview with items sorted by a CollectionViewSource.
Currently, everything is working except sorting using this code in my resource dictionary:
<HierarchicalDataTemplate DataType="{x:Type books:Container}" ItemsSource="{Binding Path=Items}">
<nav:ContainerControl />
</HierarchicalDataTemplate>
What would be the syntax for changing the HierarchicalDataTemplate to bind to a CollectionViewSource that in turn pulls from the Items property?
I've tried variations of the code posted on Bea Stollnitz's blog with no success. I can't figure out how to set the source of the CollectionViewSource.
Well let me just say that I hate my proposed solution, but it does work. Perhaps a WPF guru will enlighten us both with a better alternative. Of course if you were using a ViewModel behind your view, you could simply wrap the Items property of the model with a CollectionView in the ViewModel and be done with it.
But here's another solution. Basically, your HierarchicalDataTemplate can stay as is except you would add a Converter to the Binding. I implemented the following converter and changed the XAML accordingly.
<HierarchicalDataTemplate DataType="{x:Type books:Container}"
ItemsSource="{Binding Items, Converter={x:Static local:CollectionViewConverter.Instance}}">
<nav:ContainerControl />
</HierarchicalDataTemplate>
CollectionViewConverter.cs
public class CollectionViewConverter : IValueConverter
{
public CollectionViewConverter() {}
static CollectionViewConverter(){
Instance = new CollectionViewConverter();
}
public static CollectionViewConverter Instance {
get;
set;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var view = new ListCollectionView((System.Collections.IList)value);
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
return view;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// not really necessary could just throw notsupportedexception
var view = (CollectionView)value;
return view.SourceCollection;
}
}
I did as you suggested and wrapped the Items collection with a ListCollectionView:
private SortDescription _ItemsLcvSortDesc;
private SortDescription ItemsLcvSortDesc
{
get
{
if (_ItemsLcvSortDesc == null)
_ItemsLcvSortDesc = new SortDescription("SortOrder", ListSortDirection.Ascending);
return _ItemsLcvSortDesc;
}
}
private ListCollectionView _ItemsLcv;
public ListCollectionView ItemsLcv
{
get
{
if (_ItemsLcv == null)
_ItemsLcv = CollectionViewSource.GetDefaultView(Items) as ListCollectionView;
_ItemsLcv.SortDescriptions.Add(ItemsLcvSortDesc);
return _ItemsLcv;
}
}
Did I miss anything?
Related
I have two object: UserDto and RoleDto. User has a property which is the RoleDto.
In my viewmodel I have the following:
public UserDto User
{
get { return _user; }
set
{
if (_user == value) return;
_user = value;
User.PropertyChanged += UserPropertyChanged;
OnPropertyChanged("User");
}
}
private UserDto _user;
public IEnumerable<RoleDto> Roles { get; set; } //I load all available roles in here
In the view, I want to select the role that the user belongs. This is how I define the combobox in the view:
<ComboBox Grid.Row="3" Grid.Column="1" Margin="5" ItemsSource="{Binding Roles}" SelectedItem="{Binding User.Role, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Description" />
If I try to create a new user and select a role from the combobox, it is correctly binded to the user. The problem is that when I load a user that already exists, the role is not displayed in the combobox (even the user has a role defined).
Any help please?
Thanks in advance
This is because the reference of RoleDTO that your UserDTO has, does not match any of the RoleDTOs in Roles collection which you set as ItemsSource of ComboBox.
Better define a property on your ViewModel like
public RoleDTO SelectedRole
{
get { return Roles.FirstOrDefault(role => role.Role == User.RoleDto.Role); }
set { User.RoleDto = value; OnPropertyChanged("SelectedRole"); }
}
and set it as SelectedItem of you combobox
ItemsSource="{Binding Roles}" SelectedItem="{Binding SelectedRole, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Description" />
In my opinion the second option on this page is the easiest way.
https://rachel53461.wordpress.com/2011/08/20/comboboxs-selecteditem-not-displaying/
You can override the equals property on your object so that it returns true if the items have the same data. Then when the combo box box goes to check to make sure your item is in the selection it will find a match.
The other way to solve this problem is using Converter on Binding. when you use binding to bind SelectedItem, WPF will check the reference of SelectedItem against all objects inside ItemsSource property and of course if there was no match, SelectedItem will be empty. using Converter you can tell WPF that how it should match SelectedItem.
In this case you just need find SelectedItem among ItemsSource and return it to Binding. so follow these steps:
1- Create a class and implement IValueConverter. It has two methods: Convert and ConvertBack
2- for Convert method do something like this:
public class MySelecteItemBindingConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var mySelectedItem = value as MySelectedItemType;
var myItemsSource = parameter as List<MySelectedItemType>;
var matchedItem = myItemsSource.FirstOrDefault(i=>i.Id == mySelectedItem.Id);
return matchedItem;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
// Do just like Convert method
}
}
3- Use this Converter on your Binding like this:
var myBinding = new Binding("YourBindingPath");
myBinding.Converter = new MySelectedItemBindingConverter();
myBinding.ConverterParameter = myItemsSource; //this is List<MySelectedItemType> in this example
myCombo.SetBinding(ComboBox.SelectedItemProperty, myBinding);
Note: if you want to do binding from XAML you can not pass ConverterParameter like this, instead you should create a static list and use that as ItemsSource or use MultiBinding to pass your ConverterParameter using a trick. here there is a good and simple explanation about it: Binding ConverterParameter
I have a bound WPF comboBox that has an ItemsSource set to a CompositeCollection. I'm trying this to try and accomodate adding <Select> and <Add New...> selections to precede an ObservableCollection of 'regular' objects. What I can't figure out is how to, in code-behind, select one of these added choices.
This is how I'm building the CompositeCollection:
private CompositeCollection CreateItemsSource(ObservableCollection<T> source)
{
CompositeCollection cmpc = new CompositeCollection();
cmpc.Add(new ComboBoxItem { Content = "<Select>" });
cmpc.Add(new ComboBoxItem { Content = "<Add New...>" });
var cc1 = new CollectionContainer { Collection = source };
cmpc.Add(cc1);
return cmpc;
}
This is what the ComboBox looks like:
<DataTemplate x:Key="LookupComboTemplate">
<TextBlock Text="{Binding}"/>
</DataTemplate>
<ComboBox ItemsSource="{Binding SubCategories.ItemsSource}"
ItemTemplate="{StaticResource LookupComboTemplate}">
<ComboBox.SelectedItem>
<Binding Path="SourceData.SubCategoryObj" Mode="TwoWay"></Binding>
</ComboBox.SelectedItem>
</ComboBox>
I've got a situation where SelectedItem SourceData.SubCategoryObj is null (it's an optional property). In this case, I want to manually select and display the <Select> choice. But no matter what I do (setting SelectedIndex is ignored, setting SelectedValue to the ComboBoxItem in the CompositeCollection is ignored) I get a blank ComboBox when it renders.
I'd appreciate any advice on how I can do this.
Thanks!
Corey.
You should be able to fix this with a custom valueconverter for your SelectedItem binding. http://wpftutorial.net/ValueConverters.html should give you some pointers.
I'm not sure if combox wants a simple string or some composite object but you can check that. Something like
public class ComboConverter: IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return "<Select>";
return value;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (value.toString().Equals("<Select>")
return null;
return value;
}
should give you the "<Select>" entry if the selected item is null.
In my ViewModel, I have a Dictionary of observable string collections, declared as below.
public Dictionary<string, ObservableCollection<string>> NamePartsDict { get; set; }
I would like to bind the Dictionary to ComboBoxes in my user interface in such a way that each ComboBox can 'select' which collection to bind to.
So in my XAML, I would like to use:
<ComboBox x:Name="comboBox" IsEditable="True"
ItemsSource="{Binding CurrentLibrary.NamePartsDict[Year]}" Margin="80,0,0.871,0"></ComboBox>
which I would expect to bind to the Collection that is indexed by the Key "Year", and populate the ComboBox with the strings stored in the collection.
However, this XAML results in an empty ComboBox.
I have verified that the Dictionary itself can be bound. The below XAML populates the ComboBox with the string representations of each Key, Value pair.
<ComboBox x:Name="comboBox" IsEditable="True"
ItemsSource="{Binding CurrentLibrary.NamePartsDict}" Margin="80,0,0.871,0"></ComboBox>
Is something in my binding path wrong when obtaining the value from the dictionary? Or am I trying to do something that isn't possible (in which case I'll have to find another way!)?
Any help much appreciated!
Tim
You should use a Datatemplate here
like in here:
http://www.codeproject.com/Articles/47923/Using-a-different-DataTemple-when-a-WPF-ComboBox-i
I solved this problem by using a converter:
/// <summary>
/// Returns a
/// </summary>
public class DomainValueConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ObservableCollection<DomainValue> retVal = null;
DomainDefinitionCollection dds = value as DomainDefinitionCollection;
if (dds != null)
{
retVal = dds[parameter.ToString()];
}
return retVal;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
<controls:ComboBox VerticalAlignment="Center"
DisplayMemberPath="Name"
SelectedValuePath="Value"
SelectedValue="{Binding ChildClientEntity.EditableEntity.EditableAttributes.TeamPosition, Mode=TwoWay}"
ItemsSource="{Binding ChildClientEntity.Domains, Converter={StaticResource DomainValueConverter}, ConverterParameter=SiteVisitTeamPosition}" />
where the ConverterParameter is the key into the dictionary, Domains.
public Dictionary<string, ObservableCollection<DomainValue>> Domains { get; private set; }
I am trying to use IValueConverter to convert the collection into proxy object for data binding.
The converter seems to work fine, but the problem is when a new object is added or removed from the collection. The same is not refreshed in the view..
Model Object:
public class A {
public ObservableCollection<string> Members { get; }
}
Converter
public class MemberConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var collection = new CompositeCollection();
var a = value as A;
a.Members.ToList().ForEach(member => {
collection.Add(new ProxyClass{ A= a, Member= member });
});
return collection;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new System.NotImplementedException();
}
}
Proxy Class
public class ProxyClass {
public A A { get; set; }
public string Member{ get; set; }
}
XAML:
<DataTemplate DataType="{x:Type my:ProxyClass}">
<TextBlock Text="{Binding Path=Member}"/>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type A}" ItemsSource="{Binding Converter={StaticResource MemberConverter}}">
<TextBlock Text ="{Binding}"/>
</HierarchicalDataTemplate>
A Binding will only be re-evaluated if a property change notification for the property to which it is bound has been changed. In this case the ItemsSource is bound to the DataContext - the A instance itself - so unless it is given a new A instance it will not be re-evaluated. There is nothing listening to the change notifications that the collection raises since the value given to the ItemsSource is actually a different collection instance that you are creating within your converter.
One option would be to have the converter create a helper class that hooks the CollectionChanged event of the source collection (i.e. the value passed into the converter) and that object would be responsible for keeping the source collection and the one it creates in sync. Another option is to try to force the binding to get re-evaluated - e.g. use a Path of "Members" for the ItemsSource binding and when you change the contents of the collection raise a property change notification for "Members" on A.
Its not updating because your A Property doesn't implement INotifyPropertyChanged or is a DependencyProperty
If need be you can add the following after making it implement one of the previous.
ItemsSource="{Binding Converter={StaticResource MemberConverter}, UpdateSourceTrigger=PropertyChanged}">
Well the problem is that I have this enum, BUT I don't want the combobox to show the values of the enum. This is the enum:
public enum Mode
{
[Description("Display active only")]
Active,
[Description("Display selected only")]
Selected,
[Description("Display active and selected")]
ActiveAndSelected
}
So in the ComboBox instead of displaying Active, Selected or ActiveAndSelected, I want to display the DescriptionProperty for each value of the enum. I do have an extension method called GetDescription() for the enum:
public static string GetDescription(this Enum enumObj)
{
FieldInfo fieldInfo =
enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
{
return enumObj.ToString();
}
else
{
DescriptionAttribute attrib =
attribArray[0] as DescriptionAttribute;
return attrib.Description;
}
}
So is there a way I can bind the enum to the ComboBox AND show it's content with the GetDescription extension method?
Thanks!
I would suggest a DataTemplate and a ValueConverter. That will let you customize the way it's displayed, but you would still be able to read the combobox's SelectedItem property and get the actual enum value.
ValueConverters require a lot of boilerplate code, but there's nothing too complicated here. First you create the ValueConverter class:
public class ModeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
return ((Mode) value).GetDescription();
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
Since you're only converting enum values to strings (for display), you don't need ConvertBack -- that's just for two-way binding scenarios.
Then you put an instance of the ValueConverter into your resources, with something like this:
<Window ... xmlns:WpfApplication1="clr-namespace:WpfApplication1">
<Window.Resources>
<WpfApplication1:ModeConverter x:Key="modeConverter"/>
</Window.Resources>
....
</Window>
Then you're ready to give the ComboBox a DisplayTemplate that formats its items using the ModeConverter:
<ComboBox Name="comboBox" ...>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource modeConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
To test this, I threw in a Label too, that would show me the actual SelectedItem value, and it did indeed show that SelectedItem is the enum instead of the display text, which is what I would want:
<Label Content="{Binding ElementName=comboBox, Path=SelectedItem}"/>
I like the way you think. But GetCustomAttributes uses reflection. What is that going to do to your performance?
Check out this post:
WPF - Displaying enums in ComboBox control
http://www.infosysblogs.com/microsoft/2008/09/wpf_displaying_enums_in_combob.html
This is how I am doing it with MVVM. On my model I would have defined my enum:
public enum VelocityUnitOfMeasure
{
[Description("Miles per Hour")]
MilesPerHour,
[Description("Kilometers per Hour")]
KilometersPerHour
}
On my ViewModel I expose a property that provides possible selections as string as well as a property to get/set the model's value. This is useful if we don't want to use every enum value in the type:
//UI Helper
public IEnumerable<string> VelocityUnitOfMeasureSelections
{
get
{
var units = new []
{
VelocityUnitOfMeasure.MilesPerHour.Description(),
VelocityUnitOfMeasure.KilometersPerHour.Description()
};
return units;
}
}
//VM property
public VelocityUnitOfMeasure UnitOfMeasure
{
get { return model.UnitOfMeasure; }
set { model.UnitOfMeasure = value; }
}
Furthermore, I use a generic EnumDescriptionCoverter:
public class EnumDescriptionConverter : IValueConverter
{
//From Binding Source
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is Enum)) throw new ArgumentException("Value is not an Enum");
return (value as Enum).Description();
}
//From Binding Target
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is string)) throw new ArgumentException("Value is not a string");
foreach(var item in Enum.GetValues(targetType))
{
var asString = (item as Enum).Description();
if (asString == (string) value)
{
return item;
}
}
throw new ArgumentException("Unable to match string to Enum description");
}
}
And finally, with the view I can do the following:
<Window.Resources>
<ValueConverters:EnumDescriptionConverter x:Key="enumDescriptionConverter" />
</Window.Resources>
...
<ComboBox SelectedItem="{Binding UnitOfMeasure, Converter={StaticResource enumDescriptionConverter}}"
ItemsSource="{Binding VelocityUnitOfMeasureSelections, Mode=OneWay}" />
I suggest you use a markup extension I had already posted here, with just a little modification :
[MarkupExtensionReturnType(typeof(IEnumerable))]
public class EnumValuesExtension : MarkupExtension
{
public EnumValuesExtension()
{
}
public EnumValuesExtension(Type enumType)
{
this.EnumType = enumType;
}
[ConstructorArgument("enumType")]
public Type EnumType { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (this.EnumType == null)
throw new ArgumentException("The enum type is not set");
return Enum.GetValues(this.EnumType).Select(o => GetDescription(o));
}
}
You can then use it like that :
<ComboBox ItemsSource="{local:EnumValues local:Mode}"/>
EDIT: the method I suggested will bind to a list of string, which is not desirable since we want the SelectedItem to be of type Mode. It would be better to remove the .Select(...) part, and use a binding with a custom converter in the ItemTemplate.
Questions of using reflection and attributes aside, there are a few ways you could do this, but I think the best way is to just create a little view model class that wraps the enumeration value:
public class ModeViewModel : ViewModel
{
private readonly Mode _mode;
public ModeViewModel(Mode mode)
{
...
}
public Mode Mode
{
get { ... }
}
public string Description
{
get { return _mode.GetDescription(); }
}
}
Alternatively, you could look into using ObjectDataProvider.
I've done it like this :
<ComboBox x:Name="CurrencyCodeComboBox" Grid.Column="4" DisplayMemberPath="." HorizontalAlignment="Left" Height="22" Margin="11,6.2,0,10.2" VerticalAlignment="Center" Width="81" Grid.Row="1" SelectedValue="{Binding currencyCode}" >
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
in code I set itemSource :
CurrencyCodeComboBox.ItemsSource = [Enum].GetValues(GetType(currencyCode))