I have an enum where some of the items have a DescriptionAttribute set.
I want to in my WPF app have a dropdown where the user can select an items from the enum, but I want the dropdown to utilize the Description value if it's available.
I wrote code to get the list of values (pull the description if available, else use name), and I have the XAML I'm trying to use for the Object Provider, but it doesn't populate anything.
The XAML works if I use GetValues with ObjectType Definition.
C#
public static string[] GetDescriptions(Enum enumType)
{
List<string> descriptions = new List<string>();
Type t = enumType.GetType();
foreach(string name in Enum.GetNames(t))
{
FieldInfo field = t.GetField(name);
object[] d = field.GetCustomAttributes(typeof(DescriptionAttribute), true);
if (d.Any())
{
descriptions.Add(((DescriptionAttribute)d[0]).Description);
}
else
{
descriptions.Add(name);
}
}
return descriptions.ToArray();
}
XAML:
<ObjectDataProvider x:Key="SkillEnum" MethodName="KwCommon:EnumExtensions.GetDescriptions" >
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:SkillLevels"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
I do something similar to bind an enum to a combobox. I have a couple of helper functions:
public static string GetEnumDescription<TEnum>(this TEnum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes != null && attributes.Length > 0)
{
return attributes[0].Description;
}
else
{
return value.ToString();
}
}
and this function that turns the enum into a list of keyvaluepairs:
public static IEnumerable<KeyValuePair<string, Enum>> GetEnumList(this Enum t, bool useDescription = true)
{
return Enum.GetValues(t.GetType()).Cast<Enum>().Select(e => new KeyValuePair<string, Enum>(useDescription == true ? e.GetEnumDescription() : e.ToString(), e)).ToList();
}
I create a converter (In my case markupextension, here is a link for that markupextension)
public class EnumToListConverter : ConverterMarkupExtension<EnumToListConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool useDescription = true;
if (parameter is bool p)
{
useDescription = p;
}
if (value is Enum e)
{
return EnumHelper.GetEnumList(e, useDescription);
}
return DependencyProperty.UnsetValue;
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
Then in my xaml I use it like this:
<ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Applicant.RentOrOwn, Converter={local:EnumToListConverter}, Mode=OneTime}" SelectedValuePath="Value" DisplayMemberPath="Key" SelectedValue="{Binding Applicant.RentOrOwn, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, ValidatesOnNotifyDataErrors=True}"></ComboBox>
In this example I have an Applicant class which has a RentOrOwn enum property. This will show all the values available in the enum, and then update the RentOrOwn property when the user clicks the new selected value in an MVVM style.
Related
I have a Control that I want to automatically disappear if another control has no visibile children. I'm not sure how to implement that though. I feel as though I need to create a binding that returns bindings for each child element's visible property and then aggregates them into a MultiValueConverter. I think it is working but it seems as though when I add items to my collection, the collection binding isn't being re-evaluated. Has anyone done this before?
Below is my code:
<Grid.Resources>
<local:BindingExpander x:Key="BindingExpander"/>
<local:TestConverter x:Key="TestConverter" />
</Grid.Resources>
<Button Content="Button" HorizontalAlignment="Left" Margin="237,166,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click">
<Button.Visibility>
<MultiBinding Converter="{StaticResource TestConverter}">
<Binding ElementName="lstItems" Path="Items" Converter="{StaticResource BindingExpander}" ConverterParameter="Visibility"/>
</MultiBinding>
</Button.Visibility>
</Button>
<ListBox x:Name="lstItems" HorizontalAlignment="Left" Height="100" Margin="601,130,0,0" VerticalAlignment="Top" Width="100" DisplayMemberPath="Content"/>
and:
public class TestConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
var ret = Visibility.Collapsed;
foreach (var item in values) {
if(item is IEnumerable IE) {
foreach (var Child in IE) {
}
}
}
return ret;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public class BindingExpander : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var ret = new List<Binding>();
if(value is IEnumerable IE) {
foreach (var item in IE) {
ret.Add(new Binding(parameter.ToString()) {
Source = item,
Mode = BindingMode.OneWay
});
}
}
return ret;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
````
I have a Control that I want to automatically disappear if another
control has no visibile children..
Simply create a Boolean property which reports the status of what the other control is binding to such as:
public bool HasItems { get { return _SomeArray?.Any(); }}
This property can be as elaborate as needed, but a basic one above for the example is shown.
Then bind the visibility flag of the control in question to the HasItems.
Note that the HasItems does not have the plumbing for INotifyPropertyChanged. In the code(s) where items are added to the _SomeArray simply put in a call to PropertyChanged("HasItems")
On my blog I provide a basic example of that (Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding) which looks like this where someone would bind to IsMemebershipAtMax such as what you are doing:
public bool IsMembershipAtMax
{
get { return MemberCount > 3; }
}
public int MemberCount
{
get { return _MemberCount; }
set
{
_MemberCount = value;
OnPropertyChanged();
OnPropertyChanged("IsMembershipAtMax");
}
}
public List<string> Members
{
get { return _Members; }
set { _Members = value; OnPropertyChanged(); }
}
I would like to display "Yes" or "No" for whenever a Boolean type data received(it can receive different types of data) for generating a column in RadGridView instead of a checkbox. I would like to implement this changes in xaml. Columns are generating dynamically. This is how it's created now:
<telerik:RadGridView x:Name="Data" Grid.Row="3" Margin="5" AutoGenerateColumns="False" CanUserSortColumns="True" IsFilteringAllowed="True"
grid:RadGridViewColumnsBinding.ColumnsCollection="{Binding Path=ColumnsData}"
IsReadOnly="False" CanUserResizeColumns="True"/>
I am new in Silverlight coding. Will really appreciate if someone can help.
You should check out Telerik's ConditionalDataTemplateSelector they have in this demo, and read about IValueConverter if you haven't already.
Depending on what you are trying to do with all your columns, the ConditionalDataTemplateSelector might be overkill, but you can use it to create a rule system for what DataTemplate to use for a given cell based on a custom rule system.
<Grid.Resources>
...
<DataTemplate x:Key="CellDisplayTextBox">
<TextBlock Text="{Binding Value, Converter={StaticResource BooleanToYesNoConverter}}" />
</DataTemplate>
<selector:ConditionalDataTemplateSelector x:Key="displaySelector" ConditionConverter="{StaticResource someConverter}">
<selector:ConditionalDataTemplateSelector.Rules>
<selector:ConditionalDataTemplateRule DataTemplate="{StaticResource CellDisplayTextBox}">
<selector:ConditionalDataTemplateRule.Value>
<sys:Int32>1</sys:Int32> <!--You need to figure out what value and type to use here -->
</selector:ConditionalDataTemplateRule.Value>
</selector:ConditionalDataTemplateRule>
...
</selector:ConditionalDataTemplateSelector.Rules>
</Grid.Resources>
...
<telerikGridView:RadGridView>
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn CellTemplateSelector="{StaticResource displaySelector}" CellEditTemplateSelector="{StaticResource editSelector}" />
</telerik:RadGridView.Columns>
</telerikGridView:RadGridView>
The IValueConverter will let you bind a bool value, but display a string value. For a BooleanToYesNoConverter you could do something like:
public class BooleanToYesNoConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool? bValue = value as bool?;
if (bValue.HasValue)
return bValue.Value ? "Yes" : "No";
else
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string sValue = value as string;
return sValue == "Yes";
}
}
The ConditionalDataTemplateSelector code from the demo:
public class ConditionalDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
object conditionValue = this.ConditionConverter.Convert(item, null, null, null);
foreach (ConditionalDataTemplateRule rule in this.Rules)
{
if (Equals(rule.Value, conditionValue))
{
return rule.DataTemplate;
}
}
return base.SelectTemplate(item, container);
}
List<ConditionalDataTemplateRule> _Rules;
public List<ConditionalDataTemplateRule> Rules
{
get
{
if (this._Rules == null)
{
this._Rules = new List<ConditionalDataTemplateRule>();
}
return this._Rules;
}
}
IValueConverter _ConditionConverter;
public IValueConverter ConditionConverter
{
get
{
return this._ConditionConverter;
}
set
{
this._ConditionConverter = value;
}
}
}
public class ConditionalDataTemplateRule
{
object _Value;
public object Value
{
get
{
return this._Value;
}
set
{
this._Value = value;
}
}
DataTemplate _DataTemplate;
public DataTemplate DataTemplate
{
get
{
return this._DataTemplate;
}
set
{
this._DataTemplate = value;
}
}
}
When I select (by clicking or by keyboard) blank row on my DataGrid (when I want to add new row), unexpected validation error occurs (but with no exception) - the border of datagrid changes to red color, as you can see on the image below. When I click second time on blank row, the red border dissapears. Everything other works fine, the new row is added. Besides, I don't have any validation rules. And when I make a row with empty text, value is valid.
I don't want this behavior and this red border, anybody knows, why this happens and how to fix it? Why and where some validation fails?
Below I append some source code:
DataGrid definition in xaml:
<DataGrid IsSynchronizedWithCurrentItem="True" DisplayMemberPath="Name"
ItemsSource="{Binding Path=ConfigFiles}" SelectedItem="{Binding Path=SelectedConfigFile}"
Grid.Column="1" Height="87" Margin="0,26,11,32" Style="{DynamicResource DataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Width="1*" Binding="{Binding Name}" />
</DataGrid.Columns>
</DataGrid>
My ViewModel's part:
public class ManageModulesVM : BaseVM // Implements INotifyPropertyChanged
{
// ...
public ObservableCollection<ConfigFile> ConfigFiles
{
get { return selectedModule == null ? null : selectedModule.ConfigFiles; }
set
{
selectedModule.ConfigFiles = value;
OnPropertyChanged(() => ConfigFiles);
}
}
public ConfigFile SelectedConfigFile
{
get { return selectedModule == null ? null : selectedModule.SelectedConfigFile; }
set
{
if (value != null)
{
selectedModule.SelectedConfigFile = value;
}
OnPropertyChanged(() => SelectedConfigFile);
OnPropertyChanged(() => Parameters);
}
}
// ...
}
ConfigFile class:
public class ConfigFile
{
public string Name { get; set; }
public IList<Parameter> Parameters { get; set; }
public ConfigFile() { Name = ""; Parameters = new List<Parameter>(); }
}
Edit:
After further investigation I know, that SelectedItem Binding is causing problems (when I remove this binding, validation error stops to appear), but I still don't know why and how to fix this.
I've found my own solution to this question. I've written a value converter and tied it to the binding:
(SelectedItem="{Binding Path=SelectedConfigFile,Converter={StaticResource configFileConverter}}")
The converter class:
namespace Converters
{
public class SelectedConfigFileConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is ConfigFile)
return value;
return null;
}
}
}
Define resource in resources.xaml file (or in any other resources place):
<ResourceDictionary (...) xmlns:conv="clr-namespace:Converters" >
<conv:SelectedConfigFileConverter x:Key="configFileConverter" />
</ResourceDictionary>
The advantage of this solution is that the SelectedConfigFile property's type did't changed (to the general object type) so it is still strongly typed.
To get the reason, when you click the new row of DataGrid in Debug mode, please see the debug window. There are first exception messages which will give you the idea why your problem is occurred.
Yes, the problem is from type casting. You need to modify the type of SelectedItem to object type as below.
public class ManageModulesVM : BaseVM // Implements INotifyPropertyChanged
{
// ...
public object SelectedConfigFile
{
get { return selectedModule == null ? null : selectedModule.SelectedConfigFile; }
set
{
if (value != null)
{
selectedModule.SelectedConfigFile = value;
}
OnPropertyChanged(() => SelectedConfigFile);
OnPropertyChanged(() => Parameters);
}
}
// ...
}
Here's a general-purpose converter you can use for any DataGrid, binding any kind of item:
public class DataGridItemConverter : MarkupExtension, IValueConverter
{
static DataGridItemConverter converter;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value != null && value.GetType() == targetType) ? value : null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (converter == null)
converter = new DataGridItemConverter();
return converter;
}
}
Since it implements MarkupExtension you don't even need to define a static resource, you can just reference it like this:
SelectedItem="{Binding SelectedThing,Converter={conv:DataGridItemConverter}}"
You can just add this line to your DataGrid:
<DataGrid Validation.ErrorTemplate="{x:Null}" />
You can just add this line to your DataGrid:
<DataGrid Validation.ErrorTemplate="{x:Null}" />
It will solve the problem
I have a Collection of Model-objects in my ViewModel. I would like to be able to bind a TabControl to these and use a DataTemplate to extract the information from the Model-objects. When I try to do this I get the errormessage: Unable to cast object of type Model to object of type TabItem. After spending some time looking for a solution I found the following:
The Silverlight TabControl is
broken. Use a combination of ListBox
and ContentControl to mimic the
behaviour of a TabControl. (Means
that I have to skin the ListBox to
look like a TabControl)
TabControl does not override
PrepareContainerForItemOverride and
the solution is to make a
Converter. (Not so good because I
then need to specify the type of the
convertee in the Converter)
Anyone know any better solution?
XAML
<sdk:TabControl ItemsSource="{Binding Items, ElementName=MyControl}">
<sdk:TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</sdk:TabControl.ItemTemplate>
</sdk:TabControl>
C#
public ObservableCollection<Model> Items { get; set; }
public ViewModel()
Items = new ObservableCollection<Model>{
new Model { Name = "1"},
new Model { Name = "2"},
new Model { Name = "3"},
new Model { Name = "4"}
};
}
Suggested Converter:
public class TabConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
List<TabSource> source = value as List<TabSource>;
if (source != null)
{
List<TabItem> result = new List<TabItem>();
foreach (TabSource tab in source)
{
result.Add(new TabItem()
{
Header = tab.Header,
Content = tab.Content
});
}
return result;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Create converter
public class SourceToTabItemsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
var source = (IEnumerable)value;
if (source != null)
{
var controlTemplate = (ControlTemplate)parameter;
var tabItems = new List<TabItem>();
foreach (object item in source)
{
PropertyInfo[] propertyInfos = item.GetType().GetProperties();
//тут мы выбираем, то поле которое будет Header. Вы должны сами вводить это значение.
var propertyInfo = propertyInfos.First(x => x.Name == "name");
string headerText = null;
if (propertyInfo != null)
{
object propValue = propertyInfo.GetValue(item, null);
headerText = (propValue ?? string.Empty).ToString();
}
var tabItem = new TabItem
{
DataContext = item,
Header = headerText,
Content =
controlTemplate == null
? item
: new ContentControl { Template = controlTemplate }
};
tabItems.Add(tabItem);
}
return tabItems;
}
return null;
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// ConvertBack method is not supported
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("ConvertBack method is not supported");
}
Create ControlTemplate:
<ControlTemplate x:Key="MyTabItemContentTemplate">
<StackPanel>
<TextBlock Text="{Binding Path=name}" />
</StackPanel>
</ControlTemplate>
And binding convert, controltemplate
<controls:TabControl x:Name="tabControl"
ItemsSource="{Binding ElementName=tabControl,
Path=DataContext,
Converter={StaticResource ConverterCollectionToTabItems},
ConverterParameter={StaticResource MyTabItemContentTemplate}}">
</controls:TabControl>
taken from the blog binding-tabcontrol
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))