Bind Flag enums to listbox containing checkboxes - c#

I have flag enum say this -
[Flags]
public enum Department
{
None = 0,
A = 1,
B = 2,
C = 4,
D = 8
}
I want to show values of this enum on view. I thought of creating a listbox and binding its source to the collection of this enum List<Department> Departments.
All works so good until i thought of having a checkbox which binds to a property on my Viewmodel -
public Department SelectedDepartments { get; set; }
The solution here http://compilewith.net/2008/12/wpf-flagsenumvalueconverter.html provides elegant solution for binding enum values to checkboxes but its have one limitation of creating checkboxes equal to the number of enum values in list.
But, in my case i can't afford of having so many checkboxes lying on my UI since my Enum contains 20 values (so that means having 20 checkboxes on UI).
I tried using MultiBindingConverter but that fails in ConvertBack Method.
I want to bind the state of checkboxes with property SelectedDepartments. Say if property value is "A | B" then A and B checkbox should be checked whereas C and D should remain unchecked.

I don't think there's a way of doing this without using some code-behind.
I took the sample solution you linked to above, removed all of the CheckBoxes from MainWindow.xaml, added the following method to MainWindow.xaml.cs and called it from the MainWindow constructor:
private void AddCheckBoxes()
{
var converter = new FlagsEnumValueConverter();
foreach (Department dept in Enum.GetValues(typeof(Department)))
{
if (dept != Department.None)
{
var binding = new Binding()
{
Path = new PropertyPath("Department"),
Converter = converter,
ConverterParameter = dept
};
var checkBox = new CheckBox() { Content = dept.ToString() };
checkBox.SetBinding(CheckBox.IsCheckedProperty, binding);
DepartmentsPanel.Children.Add(checkBox);
}
}
}
This method does the work of creating all of the checkboxes, one for each named enum constant apart from None. I could then add further departments to the Department enum, rerun the solution and see additional checkboxes for the newly-added departments.
There were a few further minor changes that I had to make to this solution to get it working completely. You may or may not need to make these changes to your code. Firstly, I made the DataObject class implement INotifyPropertyChanged. Secondly, I rewrote the XAML in MainWindow.xaml as follows:
<StackPanel>
<StackPanel x:Name="DepartmentsPanel" />
<TextBlock Margin="5,20,0,0">
<TextBlock Text="Raw Value:" FontWeight="Bold" />
<TextBlock Text="{Binding Department}" />
</TextBlock>
</StackPanel>
(Basically, I wrapped the existing DepartmentsPanel in another StackPanel and moved the 'Raw Value' display into this outer StackPanel.) Finally, I set the DataContext of the whole MainWindow, rather than the DataContext of the DepartmentsPanel, to the DataObject created. This step was necessary to make the 'Raw Value' display work.

I have created an IValueConverter that supports binding to the enum directly without codebehind or helper classes. It has only two restrictions:
One converter instance has to be used for each source property. If the model contains more properties of the same enum type, each of them needs to use a separate converter instance. This can be done by instantiating the converters in the XAML.
When the enum type is a flags enum it has to be an Integer.
The solution is based on the empirical fact that there is always a Convert first before a ConvertBack. And there is always a Convert if a ConvertBack has changed the value. This one only works when the INotifyPropertyChanged is properly implemented on the model. So between the two calls the last known value can be stored in the converter and used in the ConvertBack method.
The converter instance should get the Type of the enum and whether it is a Flags enum or not.
<EnumToCheckedConverter x:Key="InstanceName" Type="{x:Type MyEnum}" Flags="True" />
Then the checkboxes can be bound using this converter.
<CheckBox Content="ValueText" IsChecked="{Binding Source, Converter={StaticResource InstanceName}, ConverterParameter=Value}"/>
Radio buttons can be bound by the same mechanism using an instance with Flags="False"
The source code of the converter
public class EnumToCheckedConverter : IValueConverter
{
public Type Type { get; set; }
public int? LastValue { get; private set; }
public bool Flags { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value.GetType() == Type)
{
try
{
var parameterValue = Enum.Parse(Type, parameter as string);
if (Flags == true)
{
var intParameter = (int)parameterValue;
var intValue = (int)value;
LastValue = intValue;
return (intValue & intParameter) == intParameter;
}
else
{
return Equals(parameterValue, value);
}
}
catch (ArgumentNullException)
{
return false;
}
catch (ArgumentException)
{
throw new NotSupportedException();
}
}
else if (value == null)
{
return false;
}
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is bool check)
{
if (check)
{
try
{
if (Flags == true && LastValue.HasValue)
{
var parameterValue = Enum.Parse(Type, parameter as string);
var intParameter = (int)parameterValue;
return Enum.ToObject(Type, LastValue | intParameter);
}
else
{
return Enum.Parse(Type, parameter as string);
}
}
catch (ArgumentNullException)
{
return Binding.DoNothing;
}
catch (ArgumentException)
{
return Binding.DoNothing;
}
}
else
{
try
{
if (Flags == true && LastValue.HasValue)
{
var parameterValue = Enum.Parse(Type, parameter as string);
var intParameter = (int)parameterValue;
return Enum.ToObject(Type, LastValue ^ intParameter);
}
else
{
return Binding.DoNothing;
}
}
catch (ArgumentNullException)
{
return Binding.DoNothing;
}
catch (ArgumentException)
{
return Binding.DoNothing;
}
}
}
throw new NotSupportedException();
}
}

Related

Instantiating a class based on a property set value

When presenting the user with a ComboBox with ObservableCollection<Type> as its ItemsSource, how can I instantiate a class in the property that SelectedItem is bound to?
The elements in the ElementList list in parentItem is either of a generic class type Element, or is of a type inheriting from Element (e.g. DigitalOutputButton or TrendGraph).
XAML:
<StackPanel Orientation="Horizontal">
<TextBlock Width="100" Text="Element Type:" />
<ComboBox Width="300" ItemsSource="{Binding Path=Element.ElementTypeList}"
SelectedItem="{Binding Path=Element.SelectedElementType}" />
</StackPanel>
C# code:
private static ObservableCollection<Type> _elementTypeList
= new ObservableCollection<Type> { typeof(Element), typeof(DigitalOutputButton), typeof(TrendGraph) };
public static ObservableCollection<Type> ElementTypeList { get { return _elementTypeList; } }
public Type SelectedElementType {
get { return GetType(); }
set {
if (value != GetType()) {
var parentItem = Controller.ConfigurationHandler.FindParentItem(this);
var currentItemIndex = parentItem.ElementList.IndexOf(this);
parentItem.ElementList[currentItemIndex] = new typeof(value)();
}
}
}
The above set code will not build. But is it possible to achieve this behavior in another way?
EDIT: Ok, this way works:
public Type SelectedElementType {
get { return GetType(); }
set {
if (value != GetType()) {
var parentItem = Controller.ConfigurationHandler.FindParentItem(this);
var currentItemIndex = parentItem.ElementList.IndexOf(this);
if (value == typeof(Element)) {
parentItem.ElementList[currentItemIndex] = new Element();
}
else if (value == typeof(DigitalOutputButton)) {
parentItem.ElementList[currentItemIndex] = new DigitalOutputButton();
}
else if (value == typeof(TrendGraph)) {
parentItem.ElementList[currentItemIndex] = new TrendGraph();
}
}
}
}
But it would be great it there was a way to do this that were a little more "maintenance free" (no need to edit when adding a new element type).
var instance = Activator.CreateInstance(value, Controller, ParentElementGroup, ItemLabel);
parentItem.ElementList[currentItemIndex] = (TYPE)instance;
The only missing link is the type of your collection so it can be cast. But that should be compile time knowledge.

Enum in WPF ComboxBox with localized names

I have a ComboBox listing an Enum.
enum StatusEnum {
Open = 1, Closed = 2, InProgress = 3
}
<ComboBox ItemsSource="{Binding StatusList}"
SelectedItem="{Binding SelectedStatus}" />
I want to display localized names for the enum values in English
Open
Closed
In Progress
but also in German (and other languages in the future)
Offen
Geschlossen
In Arbeit
In my ViewModel using
public IEnumerable<StatusEnum> StatusList
{
get
{
return Enum.GetValues(typeof(StatusEnum)).Cast<StatusEnum>();
}
}
only gets me the names of the enum in the code and not the translated ones.
I have general localization in place and can access them using i.e.
Resources.Strings.InProgress
which gets me the translation for the current language.
How can I bind the localization automatically?
It's an example of the simple Enum to translated string converter.
public sealed class EnumToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{ return null; }
return Resources.ResourceManager.GetString(value.ToString());
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string str = (string)value;
foreach (object enumValue in Enum.GetValues(targetType))
{
if (str == Resources.ResourceManager.GetString(enumValue.ToString()))
{ return enumValue; }
}
throw new ArgumentException(null, "value");
}
}
Also you need a MarkupExtension which will provide values:
public sealed class EnumerateExtension : MarkupExtension
{
public Type Type { get; set; }
public EnumerateExtension(Type type)
{
this.Type = type;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
string[] names = Enum.GetNames(Type);
string[] values = new string[names.Length];
for (int i = 0; i < names.Length; i++)
{ values[i] = Resources.ResourceManager.GetString(names[i]); }
return values;
}
}
Usage:
<ComboBox ItemsSource="{local:Enumerate {x:Type local:StatusEnum}}"
SelectedItem="{Binding SelectedStatus, Converter={StaticResource EnumToStringConverter}}" />
EDIT: You can make a more complex value converter and markup extension. The EnumToStringConverter can use DescriptionAttribute's to get the translated strings. And the EnumerateExtension can use TypeConverter.GetStandardValues() and a converter. This allows to get standard values of the specified type (not only Enums) and convert them to strings or something another depending on the converter.
Example:
<ComboBox ItemsSource="{local:Enumerate {x:Type sg:CultureInfo}, Converter={StaticResource CultureToNameConverter}}"
SelectedItem="{Binding SelectedCulture, Converter={StaticResource CultureToNameConverter}}" />
EDIT: The more complex solution described above is published on GitHub now.
You can do using a Attribute for the enum and writing an extension method for the enum. Refer the below code.
<ComboBox Width="200" Height="25" ItemsSource="{Binding ComboSource}"
DisplayMemberPath="Value"
SelectedValuePath="Key"/>
public class MainViewModel
{
public List<KeyValuePair<Status, string>> ComboSource { get; set; }
public MainViewModel()
{
ComboSource = new List<KeyValuePair<Status, string>>();
Status st=Status.Open;
ComboSource = re.GetValuesForComboBox<Status>();
}
}
public enum Status
{
[Description("Open")]
Open,
[Description("Closed")]
Closed,
[Description("InProgress")]
InProgress
}
public static class ExtensionMethods
{
public static List<KeyValuePair<T, string>> GetValuesForComboBox<T>(this Enum theEnum)
{
List<KeyValuePair<T, string>> _comboBoxItemSource = null;
if (_comboBoxItemSource == null)
{
_comboBoxItemSource = new List<KeyValuePair<T, string>>();
foreach (T level in Enum.GetValues(typeof(T)))
{
string Description = string.Empty;
FieldInfo fieldInfo = level.GetType().GetField(level.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes != null && attributes.Length > 0)
{
Description = GetDataFromResourceFile(attributes.FirstOrDefault().Description);
}
KeyValuePair<T, string> TypeKeyValue = new KeyValuePair<T, string>(level, Description);
_comboBoxItemSource.Add(TypeKeyValue);
}
}
return _comboBoxItemSource;
}
public static string GetDataFromResourceFile(string key)
{
//Do you logic to get from resource file based on key for a language.
}
}
I have already posted a similar thing in SO Is it possible to databind to a Enum, and show user-friendly values?
You can't, out of the box.
But you can create an ObservableList<KeyValuePair<StatusEnum, string>> property and fill it with your enum/localized text and then bind it to your ComboBox.
As for the string itself:
var localizedText = (string)Application.Current.FindResource("YourEnumStringName");
Getting the Enum string representation with Enum.GetName/Enum.GetNames methods.

Using localized enum as DataSource

I've lots of enums in my app. Most of them are used on combos like this:
Enum.GetValues(typeof(TipoControlador))
Now I'd like to localize them like this: Localizing enum descriptions attributes
How can I combine them? My first thought was to override the ToString method with an extension method, but that's not possible =(
Using the other article as a basis, you can create an extension method like this:
public static class LocalizedEnumExtensions
{
private static ResourceManager _resources = new ResourceManager("MyClass.myResources",
System.Reflection.Assembly.GetExecutingAssembly());
public static IEnumerable<string> GetLocalizedNames(this IEnumerable enumValues)
{
foreach(var e in enumValues)
{
string localizedDescription = _resources.GetString(String.Format("{0}.{1}", e.GetType(), e));
if(String.IsNullOrEmpty(localizedDescription))
{
yield return e.ToString();
}
else
{
yield return localizedDescription;
}
}
}
}
You would use it like this:
Enum.GetValues(typeof(TipoControlador)).GetLocalizedNames();
Technically, this extension method will accept any array, and you can't restrict it to only work on an enum, but you could add extra validation inside the extension method if you feel it's important:
if(!e.GetType().IsEnum) throw new InvalidOperationException(String.Format("{0} is not a valid Enum!", e.GetType()));
You have 2 problems here, the first is how to localize enums which is solved by Localizing enum descriptions attributes.
The second is how to display the localized name whilst using the enum's value. This can be solved by creating a simple wrapper object such as:
public sealed class NamedItem
{
private readonly string name;
private readonly object value;
public NamedItem (string name, object value)
{
this.name = name;
this.value = value;
}
public string Name { get { return name; } }
public object Value { get { return value; } }
public override string ToString ()
{
return name;
}
}
This provides a generic re-usable class for any drop down box where you might want to show a different name for an item than the item itself provides (eg enums, ints, etc).
Once you have this class, you can set the drop down's DisplayMember to Name and ValueMember to Value. This will mean that dropdown.SelectedValue will still return your enum.
I know this question is old, but this may help some people.
You can just handle the Format event of the ComboBox control (http://msdn.microsoft.com/en-us/library/system.windows.forms.listcontrol.format.aspx), and add your text logic in it.
private void ComboBoxFormat(object sender, ListControlConvertEventArgs e)
{
e.Value = GetDescription(e.Value);
}
public static string GetDescription(object item)
{
string desc = null;
Type type = item.GetType();
MemberInfo[] memInfo = type.GetMember(item.ToString());
if (memInfo != null && memInfo.Length > 0)
{
object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attrs != null && attrs.Length > 0)
{
desc = (attrs[0] as DescriptionAttribute).Description;
}
}
if (desc == null) // Description not found
{
desc = item.ToString();
}
return desc;
}
With this, the ComboBox control still holds enum values rather than strings.

MultiBinding doesn't pick up the second Property

I have a problem with the MultiBinding. It seems the latest value of the second property is not picked up when the first property changes.
<Image Width="16" Source="../Images/YellowScales.png" Grid.Column="1" >
<Image.Visibility>
<MultiBinding Converter="{Converters:GoldScaleConverter}">
<Binding Path="IsFavourite"/>
<Binding Path="MemoryUsageLevel"/>
</MultiBinding>
</Image.Visibility>
</Image>
In the ViewModel:
public bool IsFavourite
{
get { return _isFavourite; }
set
{
if (_isFavourite == value)
return;
_isFavourite = value;
RaisePropertyChanged("IsFavourite");
UnloadBookCommmand.RaiseCanExecuteChanged();
}
}
public double MemoryUsageLevel
{
get
{
return GetMemoryUsageLevel(this);
}
}
Initially when I start the app, both properties are hit from the Converter and it works as expected.
However once the app is running and I change the IsFavourite property, it does trigger the multibinding and I can see withing the Converter that IsFavourite has flipped but the second value that is MemoryUsageLevel is always 0.0. The getter is not hit again.
But why I thought the MultiBinding is meant to check the latest value of both Bindings?
This is the converter:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
bool isFavourite = (bool) values[0];
double memoryWarningLevel = (double) values[1];
if(isFavourite && (memoryWarningLevel >= 50.00 && memoryWarningLevel < 75.00))
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
You probably have to raise PropertyChanged notification for MemoryUsageLevel as well. You can raise this in the setter of IsFavourite
public bool IsFavourite
{
get { .. }
set {
...
RaisePropertyChanged("IsFavourite");
RaisePropertyChanged("MemoryUsageLevel");
}
}

How to create a property to store the index of the selected value from another property?

I need help with the following problem:
I have a class with two properties.
private byte m_selectedValue;
public byte SelectedValue
{
get { return m_selectedValue; }
set { m_selectedValue = value; }
}
private string[] m_possibleValues;
public string[] PossibleValues
{
get { return m_possibleValues; }
set { m_possibleValues = value; }
}
The PossibleValues stores the list of the selectable values. The SelectedValue contains the index of the actually selected value.
In this state the property editor shows the index of the selected value. I would like to select the value using a combobox in the property grid, the same style used with an Enum property. The combobox's list would be populated from the PossibleValues property.
With the help of this article (http://www.codeproject.com/KB/cpp/UniversalDropdownEditor.aspx) I have managed to create a custom editor that show the combobox on the property grid with the values from the PossibleValues property. I can also select the value, but still the property grid shows the index of the value instead of the value itself.
This is the modified source of the editor (original is from CodeProject):
public class EnumParamValuesEditor : UITypeEditor
{
private IWindowsFormsEditorService edSvc;
public override UITypeEditorEditStyle GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
if ((context != null) && (context.Instance != null))
return UITypeEditorEditStyle.DropDown;
return UITypeEditorEditStyle.None;
}
public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if ((context == null) || (provider == null) || (context.Instance == null))
return base.EditValue(provider, value);
edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (edSvc == null)
return base.EditValue(provider, value);
ListBox lst = new ListBox();
PrepareListBox(lst, context);
lst.SelectedIndex = (byte)value;
edSvc.DropDownControl(lst);
if (lst.SelectedItem == null)
value = null;
else
value = (byte)lst.SelectedIndex;
return value;
}
private void PrepareListBox(ListBox lst, ITypeDescriptorContext context)
{
lst.IntegralHeight = true;
string[] coll = ((EnumTerminalParam)context.Instance).PossibleValues;
if (lst.ItemHeight > 0)
{
if ((coll != null) && (lst.Height / lst.ItemHeight < coll.Length))
{
int adjHei = coll.Length * lst.ItemHeight;
if (adjHei > 200)
adjHei = 200;
lst.Height = adjHei;
}
}
else
lst.Height = 200;
lst.Sorted = true;
FillListBoxFromCollection(lst, coll);
lst.SelectedIndexChanged += new EventHandler(lst_SelectedIndexChanged);
}
void lst_SelectedIndexChanged(object sender, EventArgs e)
{
if (edSvc == null)
return;
edSvc.CloseDropDown();
}
public void FillListBoxFromCollection(ListBox lb, ICollection coll)
{
lb.BeginUpdate();
lb.Items.Clear();
foreach (object item in coll)
lb.Items.Add(item);
lb.EndUpdate();
lb.Invalidate();
}
}
Of course, it needs further modifications to correctly handle some situations (ex. the PossibleValues is empty).
So is it possible to show the PossibleValues[SelectedValue] instead of the SelectedValue in the property editor?
You need to attach a custom TypeConverter to your SelectedValue property and make the PossibleValues non browsable. The TypeConverter will be responsible for showing strings in the PropertyGrid instead of ints. So basically, you need to override CanConvertFrom, CanConvertTo, ConvertFrom and ConvertTo. When you want to get your custom strings, use the context argument passed to these methods and call your PossibleValues property in your target instance. That should make it. Seems you don't need any custom UITypeEditor here.
Instead of two separate properties why not tie them together within a Dictionary type. So much easier to use in this case. With your index as the key and the string[] as values. Just don't limit yourself to a byte for the index.

Categories

Resources