I'm trying to bind some property to an indexer with multiple parameters.
public decimal this[CalculationType calcType, AmountSource source]
{
get
{
var foundRemittance = this.RemittanceAmounts.FirstOrDefault(a => a.CalculationType == calcType);
if (foundRemittance != null)
{
return foundRemittance[source];
}
return 0.0m;
}
}
my binding :
Value="{Binding Path=WorkingEntity.AmountDetails[{x:Static edm:CalculationType.RRQRPC}\,{x:Static edm:AmountSource.Applicable}]}"
No matter what I do the value doesn't show up.
The whole code Here
The indexer returned value from the Watch :
I tested this and found that a binding works with an indexed property with two int parameters (e.g. this[int x, int y]), but it's not working with enums. I put PresentationTraceSources.TraceLevel=High on the binding, and for the enum indexer it tells me At level 1 - for AmountDetails[] found accessor <null> -- while a nearby binding has no trouble locating an [int, int] indexer overload on the same instance of the same class.
If I add an indexer public String this[object a, object b], it gets invoked with strings "{x:Static local:CalculationType.RRQRPC}" and "{x:Static local:AmountSource.Applicable}" for the two indexer parameters. So the XAML parser isn't parsing those x:Static things.
Here's how I'd do it. It's not quite as clean as what you wanted, but it works.
If you need to bind varying values of the two indexer parameters, you could write a somewhat similar multivalue converter and use a multibinding.
public class AmountDetailsIndexer : MarkupExtension, IValueConverter
{
public AmountDetailsIndexer()
{
}
public AmountDetailsIndexer(CalculationType ctype, AmountSource asource)
{
CalculationType = ctype;
AmountSource = asource;
}
public CalculationType CalculationType { get; set; }
public AmountSource AmountSource { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public object Convert(Object value, Type type, Object converterParameter, System.Globalization.CultureInfo cultureInfo)
{
var details = value as AmountDetails;
return details[CalculationType, AmountSource];
}
public object ConvertBack(Object value, Type type, Object converterParameter, System.Globalization.CultureInfo cultureInfo)
{
throw new NotImplementedException();
}
}
XAML:
<!-- Constructor with parameters -->
<Label
Content="{Binding AmountDetails,
Converter={local:AmountDetailsIndexer RRQRPC, Applicable}}"
/>
<!--
Parameterless constructor. More typing, but here you get intellisense
when you set the properties
-->
<Label
Content="{Binding AmountDetails,
Converter={local:AmountDetailsIndexer CalculationType=RRQRPC, AmountSource=Applicable}}"
/>
Using either this type of thing, or a multivalueconverter, I think it should be relatively trivial to write a generalized multiple indexer that uses reflection.
UPDATE
Here's the generalized version using reflection.
public class Indexer : MarkupExtension, IValueConverter
{
public Indexer(object a0)
{
_arguments.Add(a0);
}
public Indexer(object a0, object a1)
{
_arguments.Add(a0);
_arguments.Add(a1);
}
public Indexer(object a0, object a1, object a2)
{
_arguments.Add(a0);
_arguments.Add(a1);
_arguments.Add(a2);
}
public Indexer(object a0, object a1, object a2, object a3)
{
_arguments.Add(a0);
_arguments.Add(a1);
_arguments.Add(a2);
_arguments.Add(a3);
}
private List<object> _arguments = new List<object>();
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public object Convert(Object value, Type type, Object converterParameter, System.Globalization.CultureInfo cultureInfo)
{
var argTypes = _arguments.Select(p => p.GetType()).ToList();
// Indexers with the correct number of parameters
var sameAryIndexers = value.GetType().GetProperties()
.Where(prop =>
prop.Name == "Item"
// Must have same number of parameters
&& prop.GetIndexParameters().Length == argTypes.Count)
.ToList();
var indexerProperties =
sameAryIndexers
.Where(prop =>
prop.GetIndexParameters()
.Select(pi => pi.ParameterType)
.Zip(argTypes, (paramType, argType) => paramType.Equals(argType))
.All(b => b)
).ToList();
// If no exact match, try overloads. This is sketchy, if you ask me.
if (indexerProperties.Count != 1)
{
indexerProperties =
sameAryIndexers
.Where(prop =>
prop.GetIndexParameters()
.Select(pi => pi.ParameterType)
.Zip(argTypes, (paramType, argType) => paramType.IsAssignableFrom(argType))
.All(b => b)
).ToList();
}
if (indexerProperties.Count != 1)
{
var argTypeNames = String.Join(", ", argTypes.Select(t => t.Name));
throw new Exception($"Unable to resolve overload: Input arguments {argTypeNames}, {indexerProperties.Count} matching indexers.");
}
try
{
var x = indexerProperties.First().GetValue(value, _arguments.ToArray());
return x;
}
catch (Exception ex)
{
return null;
}
}
public object ConvertBack(Object value, Type type, Object converterParameter, System.Globalization.CultureInfo cultureInfo)
{
throw new NotImplementedException();
}
protected bool IsTypesAssignableFrom(IEnumerable<Type> to, IEnumerable<Type> from)
{
return to.Zip(from, (tt, tf) => tt.IsAssignableFrom(tf)).All(b => b);
}
}
XAML:
<Label
Content="{Binding AmountDetails,
Converter={local:Indexer
{x:Static local:CalculationType.RRQRPC},
{x:Static local:AmountSource.Applicable}}}"
/>
<!--
BEWARE
As far as XAML is concerned, we're passing it the strings "123" and "345".
But {Binding AmountDetails[123, 345]} already works, so hey.
-->
<Label
Content="{Binding AmountDetails,
Converter={local:Indexer 123, 345}}"
/>
You can also use a custom MarkupExtension to be able to use {x:static} (and other extensions) in indexers. Here's a simple example supporting one parameter, you'd need to adapt it to support the amount of parameters required:
public class FormattedPropertyPathExtension : MarkupExtension
{
public string Path { get; set; }
public string Parameter1 { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var formattedPath = string.Format(Path, Parameter1);
var valueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if ((valueTarget.TargetProperty as PropertyInfo)?.PropertyType == typeof(PropertyPath))
{
return new PropertyPath(formattedPath);
}
return formattedPath;
}
}
XAML:
Binding="{Binding Path={extensions:FormattedPropertyPath Path=Your.Binding.Path[{0}], Parameter1={x:Static ns:MyClass.MyKeyProperty}}}"
Related
I have defined an enum type detailing various color palettes for colorizing grayscale images, for which I am using Description attributes and a TypeConverter in order to use the description strings of the enum values for comboboxes, list boxes etc. that I am binding to this type. The enum looks like this:
// available color palettes for colorizing 8 bit grayscale images
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum ColorPalette
{
[Description("Alarm Blue")]
AlarmBlue,
[Description("Alarm Blue High")]
AlarmBlueHi,
[Description("Alarm Green")]
AlarmGreen,
[Description("Alarm Red")]
AlarmRed,
[Description("Fire")]
Fire,
[Description("Gray BW")]
GrayBW,
[Description("Ice 32")]
Ice32,
[Description("Iron")]
Iron,
[Description("Iron High")]
IronHi,
[Description("Medical 10")]
Medical10,
[Description("Rainbow")]
Rainbow,
[Description("Rainbow High")]
RainbowHi,
[Description("Temperature 256")]
Temperature256,
[Description("Nano Green")]
NanoGreen
};
The EnumDescriptionTypeConverter looks like this:
public class EnumDescriptionTypeConverter : EnumConverter
{
public EnumDescriptionTypeConverter(Type type) : base(type) { }
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
if (value != null)
{
FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
if (fieldInfo != null)
{
var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
return ((attributes.Length > 0) && (!string.IsNullOrEmpty(attributes[0].Description))) ? attributes[0].Description : value.ToString();
}
}
return string.Empty;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
Using this, I can bind the enum type to say, a combo box's ItemsSource property and have the description strings be used automatically as the combo box elements, using another custom markup extension class the code of which I don't believe is relevant here.
The problem is, that if I try to create a public dependency property on a custom control based on this enum type, it won't work. Here's an example custom control:
public class TestControl : Control
{
public ColorPalette Test1
{
get => (ColorPalette)GetValue(Test1Property);
set => SetValue(Test1Property, value);
}
public static readonly DependencyProperty Test1Property = DependencyProperty.Register(nameof(Test1), typeof(ColorPalette),
typeof(TestControl), new PropertyMetadata
{
DefaultValue = ColorPalette.Rainbow
});
}
This code compiles without error and I can put the TestControl into a window, until I try to set the value of the test property in the XAML - then I don't get the usual IntelliSense containing the enum values and when I try to manually set a value anyway, I get an Access Violation exception as soon as I run the application, right at the InitializeComponent() method of the MainWindow:
" Exception thrown at 0x00007FF84723A799 (KernelBase.dll) in .exe: 0xC0000005: Access violation reading location 0x0000000000000008. occurred "
This does not happen when I remove the TypeConverter attribute from the enum definition, but then of course the Description string binding doesn't work any more.
I don't know enough about WPF to realize what exactly the problem is. Is there a way to avoid this, and still use the TypeConverter for binding using the Description string attributes?
So I found a workaround by using a different kind of MarkupExtension as binding source for enum types:
public class EnumDescriptionBindingSourceExtension : MarkupExtension
{
public Type EnumType
{
get => enumType;
set
{
if (enumType != value)
{
if (value != null)
{
Type type = Nullable.GetUnderlyingType(value) ?? value;
if (!type.IsEnum)
throw new ArgumentException("Type must be an enum type");
}
enumType = value;
}
}
}
private Type enumType;
public EnumDescriptionBindingSourceExtension() { }
public EnumDescriptionBindingSourceExtension(Type enumType) => this.enumType = enumType;
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (enumType == null)
throw new InvalidOperationException("The enum type must be specified");
Type actualEnumType = Nullable.GetUnderlyingType(enumType) ?? enumType;
Array enumValues = Enum.GetValues(actualEnumType);
if (actualEnumType == enumType)
{
List<string> descriptions = new List<string>(enumValues.Length);
foreach (object value in enumValues)
{
FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
if (fieldInfo != null)
{
DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
descriptions.Add(((attributes.Length > 0) && !string.IsNullOrEmpty(attributes[0].Description)) ? attributes[0].Description : value.ToString());
}
}
return descriptions;
}
else
{
Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
enumValues.CopyTo(tempArray, 1);
return tempArray;
}
}
}
This extension returns an array of the description strings (if any, otherwise just value.ToString()) of the enum values. When using this in XAML bindings, I can have my combo boxes be filled with the enum value descriptions directly, while previously I would use a markup extension that would just return an array of the enum values themselves and have the conversion to their description strings be done by the TypeConverter.
When using this new markup extension, I have to use a converter that can determine an original enum value from its description string:
public class EnumDescriptionConverter : IValueConverter
{
object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Enum enumObject)
{
FieldInfo fieldInfo = enumObject.GetType().GetField(enumObject.ToString());
object[] attributes = fieldInfo.GetCustomAttributes(false);
if (attributes.Length == 0)
return enumObject.ToString();
else
{
DescriptionAttribute attribute = attributes[0] as DescriptionAttribute;
return attribute.Description;
}
}
else
throw new ArgumentException($"Conversion is only defined for enum types");
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string valString)
{
Array enumValues = targetType.GetEnumValues();
FieldInfo fieldInfo;
DescriptionAttribute[] attributes;
string target;
foreach (object enumValue in enumValues)
{
fieldInfo = enumValue.GetType().GetField(enumValue.ToString());
if(fieldInfo != null)
{
attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
target = ((attributes.Length == 1) && !string.IsNullOrEmpty(attributes[0].Description)) ? attributes[0].Description : enumValue.ToString();
if (valString == target)
return enumValue;
}
}
throw new ArgumentException($"Back-conversion failed - no enum value corresponding to string");
}
else
throw new ArgumentException($"Back-conversion is only defined for string type");
}
}
With both of these I can do for example the following in XAML:
<ns:EnumDescriptionConverter x:Key="enumDescriptionConverter"/>
(...)
<ComboBox ItemsSource="{Binding Source={ns:EnumDescriptionBindingSource {x:Type ns:MyEnumType}}, Mode=OneTime}" SelectedItem="{Binding MyEnumTypeProperty, Converter={StaticResource enumDescriptionConverter}}"/>
Which will automatically fill the combo box with the enum values, represented by their description strings, and bind the selected item to a property of that type. This then works without setting the TypeConverter attribute on the enum definition and thus my original problem doesn't occur.
I'm still none the wiser why it happened in the first place or if there's a better way to solve it but hey, it works.
do you must use dependency property?
For this cases I used ViewModel with Enum object and IValueConverter in XAML code
example of ViewModel for Enum type
public abstract class VM_PropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName)
{
var handler = PropertyChanged;
if (PropertyChanged != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class VM_EnumItem<T> : VM_PropertyChanged
{
public T Enum { get; }
public bool IsEnabled
{
get { return isEnabled; }
set { isEnabled = value; OnPropertyChange(nameof(IsEnabled)); }
}
private bool isEnabled;
public VM_EnumItem(T Enum, bool IsEnabled)
{
this.Enum = Enum;
this.IsEnabled = IsEnabled;
}
public override int GetHashCode()
{
return Enum.GetHashCode();
}
public override bool Equals(object obj)
{
if (obj != null && obj is VM_EnumItem<T> item)
return System.Enum.Equals(item.Enum, this.Enum);
return false;
}
public override string ToString()
{
return string.Format("{0} | {1}", Enum, IsEnabled);
}
}
example of ViewModel for WPF Control
class ViewModel : VM_PropertyChanged
{
public enum ColorPalette
{
[Description("Alarm Blue")]
AlarmBlue,
[Description("Alarm Blue High")]
AlarmBlueHi
}
// all options
public ObservableCollection<VM_EnumItem<ColorPalette>> EnumItems { get; } = new ObservableCollection<VM_EnumItem<ColorPalette>>()
{
new VM_EnumItem<ColorPalette>(ColorPalette.AlarmBlue, true),
new VM_EnumItem<ColorPalette>(ColorPalette.AlarmBlueHi, true)
};
public VM_EnumItem<ColorPalette> SelectedEnumItem
{
get { return EnumItems.Where(s => s.Enum == SelectedEnum).FirstOrDefault(); }
set { SelectedEnum = value.Enum; OnPropertyChange(nameof(SelectedEnumItem)); }
}
private ColorPalette SelectedEnum; // your selected Enum
}
example of Converter
public class VM_Converter_EnumDescription : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Type type = value.GetType();
if (!type.IsEnum)
return value;
string name = Enum.GetName(type, value);
FieldInfo fi = type.GetField(name);
DescriptionAttribute descriptionAttrib = (DescriptionAttribute)Attribute.GetCustomAttribute(fi, typeof(DescriptionAttribute));
return descriptionAttrib == null ? value.ToString() : descriptionAttrib.Description;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
example of WPF Control
<Window.Resources>
<ResourceDictionary >
<local:VM_Converter_EnumDescription x:Key="Converter_EnumDescription"/>
</ResourceDictionary>
</Window.Resources>
////////////
<ComboBox
ItemsSource="{Binding Path=EnumItems, Mode=OneWay}"
SelectedItem="{Binding Path=SelectedEnumItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Path=Enum, Converter={StaticResource Converter_EnumDescription}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsEnabled" Value="{Binding Path=IsEnabled}"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
In XAML (specifically on Universal Windows Platform) I can data bind to an indexed property using the property path notation for indexers.
e.g. Given a data source of type Dictionary<string,string> I can bind to an indexed property as follows:
<TextBlock Text="{Binding Dictionary[Property]}"/>
In the above, the TextBlock's DataContext is set to an object with a dictionary property, say the following:
public class DataObject
{
public Dictionary<string,string> Dictionary { get; } = new Dictionary<string,string> {["Property"]="Value"};
}
My question is, is it possible to bind to the indexed property without using the indexer notation, but instead using the syntax for standard property binding?
i.e.
<TextBlock Text="{Binding Dictionary.Property}"/>
From my initial tests this doesn't seem to work. Is there an easy way to make it work? I want to use a data source object with an indexed property (like the Dictionary in this case but could just be a simple object). I don't want to use dynamic objects.
There is no syntax that does what you want, I'm afraid. The syntax for standard property binding works for standard properties, e.g.,
<TextBlock Text="{Binding Dictionary.Count}" />
...will bind to the Count property of the dictionary (or whatever object). You need them brackets...
EDIT
If you really hate the brackets, the closest thing I can find would be to use a converter with a parameter. For example:
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
IDictionary<string, string> dictionary = value as IDictionary<string, string>;
string dictionaryValue;
if (dictionary != null && dictionary.TryGetValue(parameter as string, out dictionaryValue))
{
return dictionaryValue;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
XAML:
<Page
x:Class="UWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uwp="using:UWP">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.Resources>
<uwp:MyConverter x:Key="MyConverter" />
</Grid.Resources>
<Grid.DataContext>
<uwp:DataObject />
</Grid.DataContext>
<TextBlock
Text="{Binding Dictionary, ConverterParameter=Property1, Converter={StaticResource MyConverter}}" />
</Grid>
</Page>
...which is a roundabout way of ending up with something harder to read than the brackets.
A long time ago there was this ObservableDictionary class in Windows 8 app templates that does what you want. It's not a Dictionary<string, string>, but you can copy values from your Dictionary into the ObservableDictionary
Usage
Declare an ObservableDictionary property in your ViewModel, then bind to its indexer like usual properties:
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Foundation.Collections;
namespace MyApp
{
public class ObservableDictionary : IObservableMap<string, object>
{
private class ObservableDictionaryChangedEventArgs : IMapChangedEventArgs<string>
{
public ObservableDictionaryChangedEventArgs(CollectionChange change, string key)
{
this.CollectionChange = change;
this.Key = key;
}
public CollectionChange CollectionChange { get; private set; }
public string Key { get; private set; }
}
private Dictionary<string, object> _dictionary = new Dictionary<string, object>();
public event MapChangedEventHandler<string, object> MapChanged;
private void InvokeMapChanged(CollectionChange change, string key)
{
var eventHandler = MapChanged;
if (eventHandler != null)
{
eventHandler(this, new ObservableDictionaryChangedEventArgs(change, key));
}
}
public void Add(string key, object value)
{
this._dictionary.Add(key, value);
this.InvokeMapChanged(CollectionChange.ItemInserted, key);
}
public void Add(KeyValuePair<string, object> item)
{
this.Add(item.Key, item.Value);
}
public bool Remove(string key)
{
if (this._dictionary.Remove(key))
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
return true;
}
return false;
}
public bool Remove(KeyValuePair<string, object> item)
{
object currentValue;
if (this._dictionary.TryGetValue(item.Key, out currentValue) &&
Object.Equals(item.Value, currentValue) && this._dictionary.Remove(item.Key))
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, item.Key);
return true;
}
return false;
}
public virtual object this[string key]
{
get
{
return this._dictionary[key];
}
set
{
this._dictionary[key] = value;
this.InvokeMapChanged(CollectionChange.ItemChanged, key);
}
}
public void Clear()
{
var priorKeys = this._dictionary.Keys.ToArray();
this._dictionary.Clear();
foreach (var key in priorKeys)
{
this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
}
}
public ICollection<string> Keys
{
get { return this._dictionary.Keys; }
}
public bool ContainsKey(string key)
{
return this._dictionary.ContainsKey(key);
}
public bool TryGetValue(string key, out object value)
{
return this._dictionary.TryGetValue(key, out value);
}
public ICollection<object> Values
{
get { return this._dictionary.Values; }
}
public bool Contains(KeyValuePair<string, object> item)
{
return this._dictionary.Contains(item);
}
public int Count
{
get { return this._dictionary.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return this._dictionary.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this._dictionary.GetEnumerator();
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
int arraySize = array.Length;
foreach (var pair in this._dictionary)
{
if (arrayIndex >= arraySize) break;
array[arrayIndex++] = pair;
}
}
}
}
class DictionaryXamlWrapper : DynamicObject, INotifyPropertyChanged
{
#region PropertyChangedFunctional
public void OnPropertyChanged([CallerMemberName] string aProp = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(aProp));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
Dictionary<string, object> members = new Dictionary<string, object>();
// установка свойства
public override bool TrySetMember(SetMemberBinder binder, object value)
{
members[binder.Name] = value;
OnPropertyChanged(binder.Name);
return true;
}
// получение свойства
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = null;
if (members.ContainsKey(binder.Name))
{
result = members[binder.Name];
return true;
}
return false;
}
}
Of course, you can delete INotifyPropertyChanged functional if you do not need it.
And usage as you wanted
<TextBlock Text="{Binding DictionaryXamlWrapper.TestField}"/>
I have a property composed of email addresses of operators currently editing a record:
public ReactiveList<string> Operators { get; set; }
On the view side, I have a ListView of records, and for each of them an icon shows if the current user is an editing operator.
<FontIcon Glyph="" Visibility="{Binding Operators,
Converter={StaticResource IsUserEditingToVisibilityConverter} }" />
My problem is that the Convert() method of IsUserEditingToVisibilityConverter is not triggered when an update occurs in Operators. A TextBlock I set for debugging purpose does update though:
<TextBlock Text="{Binding Operators[0]}" />
Here's the code of IsUserEditingToVisibilityConverter:
// Taken from https://blogs.msdn.microsoft.com/mim/2013/03/11/tips-winrt-converter-parameter-binding/
public class IsUserEditingToVisibilityConverter : DependencyObject, IValueConverter
{
public UserVm CurrentUser
{
get { return (UserVm)GetValue(CurrentUserProperty); }
set { SetValue(CurrentUserProperty, value); }
}
public static readonly DependencyProperty CurrentUserProperty =
DependencyProperty.Register("CurrentUser",
typeof(UserVm),
typeof(IsUserEditingToVisibilityConverter),
new PropertyMetadata(null));
public object Convert(object value, Type targetType, object parameter, string language)
{
if (this.CurrentUser == null) return Visibility.Collapsed;
if (this.CurrentUser.EmailAddress == null) return Visibility.Collapsed;
var operators = value as IList<string>;
if (operators != null && operators.Contains(this.CurrentUser.EmailAddress))
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
The binding to Text will only update here Operators changes - as in if you were to do something like:
Operators = new ReactiveList<string>{"first", "second"};
and Operators was declared as below (which ensures that PropertyChanged is raised):
public ReactiveList<string> Operators
{
get { return _operators; }
set { this.RaiseAndSetIfChanged(ref _operators, value); }
}
It will not update if you add items to or remove items from the list.
I think you're probably doing too much in a converter here - this behaviour would be better off in the View Model. You'd have a CurrentUser property, an Operators list, and declare property using ReactiveUI's ObservableAsPropertyHelper that would update when either CurrentUser or Operators changed:
private readonly ObservableAsPropertyHelper<bool> _isUserEditing;
public ReactiveList<string> Operators { get; } = new ReactiveList<string>();
public UserVm CurrentUser
{
get { return _currentUser; }
set { this.RaiseAndSetIfChanged(ref _currentUser, value); }
}
public bool IsUserEditing => _isUserEditing.Value;
And in the constructor:
Operators.Changed.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Select(_ => WhenCurrentUserEditing())
.Switch()
.ToProperty(this, x => x.IsUserEditing, out _isUserEditing);
With WhenCurrentUserEditing implemented as:
private IObservable<bool> WhenCurrentUserEditing()
{
return this.WhenAnyValue(x => x.CurrentUser.EmailAddress)
.Select(Operators.Contains);
}
I have a record structure that I am trying to bind to a DataGrid. Basically, my columns are dynamically specified by the view model. The value that should be displayed (and edited) in grid cell must be retrieved using a function on each record (the view model behind each row of the grid).
I looked at this question and it got me half the way there. Unfortunately, I cannot use a DynamicObject because my properties on the record are namespaced names and as such cannot be represented by a string. I tried a variation where I converted this namespaced name into a bindable string (replacing all illegal characters with an underscore) but it seems like the DynamicObject would call TryGetMember every time I navigated the DataGrid (i.e. it would call the function for every cell every time I scrolled). That performance is not going to be good enough.
This is what I have so far that works for displaying data (i.e. GetValue):
public interface IDataSet
{
IEnumerable<IRecord> Records { get; }
IEnumerable<IProperty> PropertiesToDisplay { get; }
}
public interface IProperty
{
XName Name { get; }
}
public interface IRecord
{
IEnumerable<KeyValuePair<IProperty, object>> Values { get; set; }
object GetValue(IProperty property);
void SetValue(IProperty property, object value);
}
internal class SomeClass
{
DataGrid myGrid; // this is defined in the XAML
IDataSet viewModel; // this is the DataContext
private void BindToViewModelUsingConverter()
{
foreach (IProperty property in viewModel.PropertiesToDisplay)
{
Binding binding = new Binding();
binding.Converter = new ConvertMyDataConverter();
binding.ConverterParameter = property;
var column = new DataGridTextColumn
{
Header = property.Name.LocalName,
Binding = binding
};
myGrid.Columns.Add(column);
}
myGrid.ItemsSource = viewModel.Records;
}
}
internal class ConvertMyDataConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var record = value as IRecord;
var property = parameter as IProperty;
if (record != null && property != null)
{
return record.GetValue(property);
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// not sure what to do here, yet
throw new NotImplementedException();
}
}
The above code works when it comes to showing values. However, as soon as I try to edit values I get an exception:
**Two-way binding requires Path or XPath**
This makes sense because even in my converter above, I'm not sure what I would write in the ConvertBack function since I want the source to remain the same (since I'm binding to IRecord and have already called the IRecord.SetValue function to update the data.) However, it seems like ConvertBack would not give me a reference to IRecord even if I was able to do two-way binding without a path.
Is there something trivial that I am missing? I'm not averse to developing a custom control but would like some tips/ideas on how to approach the problem.
I solved this using ICustomTypeDescriptor and ITypedList. Instead of Records being an IEnumerable<IRecord>, I made it a custom collection that implemented IList<IRecord> as well as ITypedList. For the ITypedList implementation, I returned property descriptors that wrapped my IProperty implementation:
public interface IDataSet
{
TypedRecordsCollection Records { get; }
IEnumerable<IProperty> PropertiesToDisplay { get; }
}
public class TypedRecordsCollection : ObservableCollection<RecordViewModel>, ITypedList
{
private PropertyDescriptorCollection _properties;
public TypedRecordsCollection(IEnumerable<IRecord> records)
{
_properties = new PropertyDescriptorCollection(
PropertiesToDisplay
.Select(prop => new CustomPropertyDescriptor(prop))
.ToArray());
records.ForEach(rec => Items.Add(new RecordViewModel(rec, _properties)));
}
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
return _properties;
}
}
public interface IRecordViewModel : ICustomTypeDescriptor
{
object GetCellValue(IProperty property);
void SetCellValue(IProperty property, object value);
}
public class RecordViewModel : CustomTypeDescriptor, IRecord
{
private IRecord _record;
private PropertyDescriptorCollection _properties;
public RecordViewModel(IRecord record, PropertyDescriptorCollection properties)
{
_record = record;
_properties = properties;
}
public object GetCellValue(IProperty property)
{
return _record.GetValue(property);
}
public void SetCellValue(IProperty property, object value)
{
_record.SetValue(property, value);
}
public override PropertyDescriptorCollection GetProperties()
{
return _properties;
}
}
public class CustomPropertyDescriptor : PropertyDescriptor
{
private IProperty _property;
public CustomPropertyDescriptor(IProperty property)
: base(GetCleanName(property.Name), new Attribute[0])
{
_property = property;
}
public override object GetValue(object component)
{
var recordViewModel = component as IRecordViewModel;
if (recordViewModel != null)
{
return recordViewModel.GetCellValue(_property);
}
return null;
}
public override void SetValue(object component, object value)
{
var recordViewModel = component as IRecordViewModel;
if (recordViewModel != null)
{
recordViewModel.SetCellValue(_property, value);
}
}
private static string GetCleanName(XName name)
{
// need to return a XAML bindable string.. aka no "{}" or ".", etc.
}
}
Now in my XAML I can just set AutogenerateColumns to true and bind directly to the Records property without a converter.
i wanted to extend the PropertyInfo class so that it could also contain the property value, without the need to have a reference to the original object as the following
public class PropertyInfoWithValue : PropertyInfo
{
private object value;
public object Value { get; set; }
public PropertyInfoWithValue(object value)
{
this.value = value;
}
}
but now the problem is i get couple exceptions that
`PropertyInfoWithValue does not implement the inherited abstract member System.Reflection.MemberInfo/PropertyInfo.XXXX`
is there a way that i could use the same implementations within
PropertyInfo?
the way i get property info
public static IEnumerable<PropertyInfoWithValue> GetColumns<T>(
this T obj, params Expression<Func<T, object>>[] lambda)
{
HashSet<string> set = new HashSet<string>(
lambda.Select(l => (l.Body as MemberExpression).Member as PropertyInfo)
.Select(x => x.Name)
);
if (set.Count == 0)
{
return obj.GetType().GetProperties().Select(p => new PropertyInfoWithValue(p.GetValue(obj, null))).ToList();
}
else
{
return obj.GetType().GetProperties().Where(p => set.Contains(p.Name)).Select(p => new PropertyInfoWithValue(p.GetValue(obj, null))).ToList();
}
}
You would have to need to implement all methods and properties that are marked abstract by the base class PropertyInfo, but I would advice creating a custom class that reflects the data that you are trying to retreive. If you still want to return PropertyInfo because you think you need it, then perhaps a wrapper class would be easier to implement and understand.
Example:
public class PropertyInfoWithValue
{
PropertyInfo propertyInfo;
public PropertyInfoWithValue(PropertyInfo propertyInfo, object value)
{
this.propertyInfo = propertyInfo;
SetValue(value);
}
public object Value { get; private set; }
public void SetValue(object value)
{
this.Value = value;
}
public static explicit operator PropertyInfoWithValue(PropertyInfo propertyInfo)
{
if (propertyInfo == null)
return null;
// supply a default value, because we don't know it yet.
object value = null;
if (propertyInfo.PropertyType.IsValueType)
value = Activator.CreateInstance(propertyInfo.PropertyType);
return new PropertyInfoWithValue(propertyInfo, value);
}
public static explicit operator PropertyInfo(PropertyInfoWithValue
propertyInfoWithValue)
{
if (propertyInfoWithValue == null)
return null;
return propertyInfoWithValue.propertyInfo;
}
}
This way you would still be able to get the PropertyInfo by casting it back:
PropertyInfo propertyInfo = (PropertyInfo)myPropertyInfoWithValue;