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>
i'm trying to pass parameters from a viewmodel to another, how is described here:
https://mallibone.com/post/xamarin.forms-navigation-with-mvvm-light.
The problem is that when i call my NavigateAsync(string pageKey, object parameter, bool animated = true) , it's thrown a InvalidOperationException and the message setted in the navigation service is: No suitable constructor found for page.
My actual code is the following:
The page I want to navigate to:
public partial class FullScreenImagePopupPage : PopupPage
{
public FullScreenImagePopupPage(ImageSource imageSource)
{
var vm = App.Locator.FullScreenImageVM;
BindingContext = vm;
InitializeComponent();
if (imageSource != null)
vm.ImageSourceFullscreen = imageSource;
}
}
it's viewmodel:
public class FullScreenImageViewModel : BaseViewModel
{
private ImageSource _ImageSourceFullscreen { get; set; }
public ImageSource ImageSourceFullscreen
{
get { return _ImageSourceFullscreen; }
set
{
_ImageSourceFullscreen = value;
OnPropertyChanged();
}
}
public FullScreenImageViewModel(INavigationService navigationService) : base(navigationService)
{
}
}
The command called everytime i want to navigate to FullScreenImagePopupPage:
OpenFullImageCommand = new Command(async () =>await navigationService.NavigateAsync(Locator.FullScreenImagePopupPage, ImageSourceFullScreen));
The NavigateAsync function's implementation:
public async Task NavigateAsync(string pageKey, object parameter, bool animated = true)
{
var page = GetPage(pageKey, parameter);
if (page is PopupPage)
await CurrentNavigationPage.Navigation.PushPopupAsync(page as PopupPage, true);
else
{
await CurrentNavigationPage.Navigation.PushAsync(page, animated);
}
}
public Page GetPage(string pageKey, object parameter = null)
{
lock (_sync)
{
if (!_pagesByKey.ContainsKey(pageKey))
{
throw new ArgumentException(
$"No such page: {pageKey}. Did you forget to call NavigationService.Configure?");
}
var type = _pagesByKey[pageKey];
ConstructorInfo constructor;
object[] parameters;
if (parameter == null)
{
constructor = type.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(c => !c.GetParameters().Any());
parameters = new object[]
{
};
}
else
{
var a = type.GetTypeInfo()
.DeclaredConstructors;
constructor = type.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(
c =>
{
var p = c.GetParameters();
return p.Length == 1
&& p[0].ParameterType == parameter.GetType();
});
parameters = new[]
{
parameter
};
}
if (constructor == null)
{
throw new InvalidOperationException(
"No suitable constructor found for page " + pageKey);
}
var page = constructor.Invoke(parameters) as Page;
return page;
}
}
I have been looking for ways to best achieve this task and settled on this.
https://rbrundritt.wordpress.com/2012/01/30/view-object-properties-in-wpf-treeview/
It does seem to give me what i was hoping for but when i try to obtain the object properties of a thread, i end up getting a stackoverflow exception. So it seems that the objectNode class is instantiating itself in a recursive manner too many times and then something happens which causes a stackoverflow? I am not sure how to go about resolving this and making it work even for a class with many properties (like threads) and any help will be greatly appreciated.
The objectNode class is shown below
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Reflection;
namespace Server.Host
{
public class ObjectNode
{
#region Private Properties
private string _name;
private object _value;
private Type _type;
#endregion
#region Constructor
public ObjectNode(object value)
{
ParseObjectTree("root", value, value.GetType());
}
public ObjectNode(string name, object value)
{
ParseObjectTree(name, value, value.GetType());
}
public ObjectNode(object value, Type t)
{
ParseObjectTree("root", value, t);
}
public ObjectNode(string name, object value, Type t)
{
ParseObjectTree(name, value, t);
}
#endregion
#region Public Properties
public string Name
{
get { return _name; }
}
public object Value
{
get { return _value; }
}
public Type Type
{
get { return _type; }
}
public ObservableCollection<ObjectNode> Children { get; set; }
#endregion
#region Private Methods
private void ParseObjectTree(string name, object value, Type type)
{
Children = new ObservableCollection<ObjectNode>();
_type = type;
_name = name;
if (value != null)
{
if (value is string && type != typeof(object))
{
if (value != null)
{
_value = "\"" + value + "\"";
}
}
else if (value is double || value is bool || value is int || value is float || value is long || value is decimal)
{
_value = value;
}
else
{
_value = "{" + value.ToString() + "}";
}
}
PropertyInfo[] props = type.GetProperties();
if (props.Length == 0 && type.IsClass && value is IEnumerable && !(value is string))
{
IEnumerable arr = value as IEnumerable;
if (arr != null)
{
int i = 0;
foreach (object element in arr)
{
Children.Add(new ObjectNode("[" + i + "]", element, element.GetType()));
i++;
}
}
}
foreach (PropertyInfo p in props)
{
if (p.PropertyType.IsPublic)
{
if (p.PropertyType.IsClass || p.PropertyType.IsArray || p.PropertyType.IsInterface)
{
if (p.PropertyType.IsArray)
{
try
{
object v = p.GetValue(value, null);
IEnumerable arr = v as IEnumerable;
ObjectNode arrayNode = new ObjectNode(p.Name, arr.ToString(), typeof(object));
if (arr != null)
{
int i = 0, k = 0;
ObjectNode arrayNode2;
foreach (object element in arr)
{
//Handle 2D arrays
if (element is IEnumerable && !(element is string))
{
arrayNode2 = new ObjectNode("[" + i + "]", element.ToString(), typeof(object));
IEnumerable arr2 = element as IEnumerable;
k = 0;
foreach (object e in arr2)
{
arrayNode2.Children.Add(new ObjectNode("[" + k + "]", e, e.GetType()));
k++;
}
arrayNode.Children.Add(arrayNode2);
}
else
{
arrayNode.Children.Add(new ObjectNode("[" + i + "]", element, element.GetType()));
}
i++;
}
}
Children.Add(arrayNode);
}
catch { }
}
else
{
object v = p.GetValue(value, null);
if (v != null)
{
Children.Add(new ObjectNode(p.Name, v, p.PropertyType));
}
}
}
else if (p.PropertyType.IsValueType && !(value is string))
{
try
{
object v = p.GetValue(value, null);
if (v != null)
{
Children.Add(new ObjectNode(p.Name, v, p.PropertyType));
}
}
catch { }
}
}
}
}
#endregion
}
}
So from this, the TreeView can be added in the WPF simply as
<TreeView Name="ResultTreeView" BorderThickness="0">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:ObjectNode}"
ItemsSource="{Binding Path=Children}">
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal" Margin="-10,0,0,0">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text=" : "/>
<TextBlock Text="{Binding Path=Value}"/>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Then in my code, I simply write
private Foo foo = new Foo();
ObservableCollection<ObjectNode> nodes = new ObservableCollection<ObjectNode>();
nodes.Add(new ObjectNode("result", foo));
ResultTreeView.ItemsSource = nodes;
Where Foo can be any class. For most things it is working fine. But if i have a thread in the class, it will throw an exception. For example just a simple
public class Foo
{
public Foo()
{
Bar = new Thread(Baz);
}
public Thread Bar { get; set; }
private static void Baz()
{
}
}
The error that is thrown is
"An unhandled exception of type 'System.StackOverflowException' occurred in mscorlib.dll"
Once again, any advice is greatly welcomed.
ParseObjectTree method called from the constructor of ObjectNode class seems to instantiate other instances of this class recursively and endlessly. It is the reason of StackOverflowException. You need an algorithm to limit the recursion probably by counting the number of generations.
I've written a custom error message localization logic in my custom DataAnnotationsModelMetadataProvider class. It's working just fine with build-in StringLengthAttribute or RequiredAttribute validation error messages. But i am having trouble with my custom derived RegularExpressionAttribute classes. The logic i am using is something like below :
public class AccountNameFormatAttribute : RegularExpressionAttribute {
public AccountNameFormatAttribute()
: base(Linnet.Core.Shared.RegExPatterns.AccountNamePattern) {
}
public override string FormatErrorMessage(string name) {
return string.Format("{0} field must contain only letters, numbers or | . | - | _ | characters.", name);
}
}
public class SignUpViewModel {
[AccountNameFormat()]
[StringLength(16, MinimumLength = 3)]
[Required]
[DisplayName("Account Name")]
public string AccountName { get; set; }
[Required]
[DisplayName("Password")]
[StringLength(32, MinimumLength = 6)]
[DataType(System.ComponentModel.DataAnnotations.DataType.Password)]
public string Password { get; set; }
// .... and other properties, quite similar ... //
}
public class MvcDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider {
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) {
MyMvcController myMvcController = context.Controller as MyMvcController; /* custom mvc controller, that contains methods for wcf service activations and common properties. */
if (myMvcController == null) {
return base.GetValidators(metadata, context, attributes);
}
List<Attribute> newAttributes = new List<Attribute>();
foreach (Attribute att in attributes) {
if (att.GetType() != typeof(ValidationAttribute) && !att.GetType().IsSubclassOf(typeof(ValidationAttribute))) {
// if this is not a validation attribute, do nothing.
newAttributes.Add(att);
continue;
}
ValidationAttribute validationAtt = att as ValidationAttribute;
if (!string.IsNullOrWhiteSpace(validationAtt.ErrorMessageResourceName) && validationAtt.ErrorMessageResourceType != null) {
// if resource key and resource type is already set, do nothing.
newAttributes.Add(validationAtt);
continue;
}
string translationKey = "MvcModelMetaData.ValidationMessages." + metadata.ModelType.Name + (metadata.PropertyName != null ? "." + metadata.PropertyName : string.Empty) + "." + validationAtt.GetType().Name;
string originalText = validationAtt.FormatErrorMessage("{0}"); /* non-translated default english text */
// clonning current attiribute into a new attribute
// not to ruin original attribute for later usage
// using Activator.CreateInstance and then mapping with AutoMapper inside..
var newAtt = this.CloneValidationAttiribute(validationAtt);
// fetching translation from database via WCF service...
// At this point, i can see error strings are always translated.
// And it works perfect with [Required], [StringLength] and [DataType] attributes.
// But somehow it does not work with my AccountNameFormatAttribute on the web page, even if i give it the translated text as expected..
// Even if its ErrorMessage is already set to translated text,
// it still displays the original english text from the overridden FormatErrorMessage() method on the web page.
// It is the same both with client side validation or server side validation.
// Seems like it does not care the ErrorMessage that i manually set.
newAtt.ErrorMessage = myMvcController.Translations.GetTranslation(translationKey, originalText);
newAttributes.Add(newAtt);
}
IEnumerable<ModelValidator> result = base.GetValidators(metadata, context, newAttributes);
return result;
}
private ValidationAttribute CloneValidationAttiribute(ValidationAttribute att) {
if (att == null) {
return null;
}
Type attType = att.GetType();
ConstructorInfo[] constructorInfos = attType.GetConstructors();
if (constructorInfos == null || constructorInfos.Length <= 0) {
// can not close..
return att;
}
if (constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
// clone with no constructor paramters.
return CloneManager.CloneObject(att) as ValidationAttribute;
}
// Validation attributes that needs constructor paramters...
if (attType == typeof(StringLengthAttribute)) {
int maxLength = ((StringLengthAttribute)att).MaximumLength;
return CloneManager.CloneObject(att, maxLength) as StringLengthAttribute;
}
return att;
}
}
public class CloneManager {
public static object CloneObject(object input) {
return CloneObject(input, null);
}
public static object CloneObject(object input, params object[] constructorParameters) {
if (input == null) {
return null;
}
Type type = input.GetType();
if (type.IsValueType) {
return input;
}
ConstructorInfo[] constructorInfos = type.GetConstructors();
if (constructorInfos == null || constructorInfos.Length <= 0) {
throw new LinnetException("0b59079b-3dc4-4763-b26d-651bde93ba56", "Object type does not have any constructors.", false);
}
if ((constructorParameters == null || constructorParameters.Length <= 0) && !constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
throw new LinnetException("f03be2b9-b629-4a72-b025-c7a87924d9a4", "Object type does not have any constructor without parameters.", false);
}
object newObject = null;
if (constructorParameters == null || constructorParameters.Length <= 0) {
newObject = Activator.CreateInstance(type);
} else {
newObject = Activator.CreateInstance(type, constructorParameters);
}
return MapProperties(input, newObject);
}
private static object MapProperties(object source, object destination) {
if (source == null) {
return null;
}
Type type = source.GetType();
if (type != destination.GetType()) {
throw new LinnetException("e67bccfb-235f-42fc-b6b9-55f454c705a8", "Use 'MapProperties' method only for object with same types.", false);
}
if (type.IsValueType) {
return source;
}
var typeMap = AutoMapper.Mapper.FindTypeMapFor(type, type);
if (typeMap == null) {
AutoMapper.Mapper.CreateMap(type, type);
}
AutoMapper.Mapper.Map(source, destination, type, type);
return destination;
}
}
Seems like my logic was actually an odd approach.
I've discovered making custom DataAnnotationsModelValidators for each type of validation attiributes. And then translating the ErrorMessages inside Validate() and GetClientValidationRules() methods.
public class MvcRegularExpressionAttributeAdapter : RegularExpressionAttributeAdapter {
public MvcRegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute)
: base(metadata, context, attribute) {
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
return MvcValidationResultsTranslation.TranslateClientValidationRules(base.GetClientValidationRules(), this.Metadata, this.ControllerContext, this.Attribute);
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
return MvcValidationResultsTranslation.TranslateValidationResults(base.Validate(container), this.Metadata, this.ControllerContext, this.Attribute);
}
}
public class MvcValidationResultsTranslation {
public static IEnumerable<ModelClientValidationRule> TranslateClientValidationRules(IEnumerable<ModelClientValidationRule> validationRules, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
if (validationRules == null) {
return validationRules;
}
MvcController mvcController = context.Controller as MvcController;
if (mvcController == null) {
return validationRules;
}
if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
// if resource key and resource type is set, do not override..
return validationRules;
}
string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
foreach (var validationRule in validationRules) {
List<string> msgParams = new List<string>();
msgParams.Add(!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName);
if (validationRule.ValidationParameters != null) {
msgParams.AddRange(validationRule.ValidationParameters.Where(p => p.Value.GetType().IsValueType || p.Value.GetType().IsEnum).Select(p => p.Value.ToString()));
}
validationRule.ErrorMessage = string.Format(translatedText, msgParams.ToArray());
}
return validationRules;
}
public static IEnumerable<ModelValidationResult> TranslateValidationResults(IEnumerable<ModelValidationResult> validationResults, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
if (validationResults == null) {
return validationResults;
}
MvcController mvcController = context.Controller as MvcController;
if (mvcController == null) {
return validationResults;
}
if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
// if resource key and resource type is set, do not override..
return validationResults;
}
string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
List<ModelValidationResult> newValidationResults = new List<ModelValidationResult>();
foreach (var validationResult in validationResults) {
ModelValidationResult newValidationResult = new ModelValidationResult();
newValidationResult.Message = string.Format(translatedText, (!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName));
newValidationResults.Add(newValidationResult);
}
return newValidationResults;
}
}
You can use my Griffin.MvcContrib to get easier localization.
Use nuget to download griffin.mvccontrib
Define a string table as described here.
Use the regular [RegularExpression] attribute directly in your view model.
Add this to your string table:
SignUpViewModel_AccountName_RegularExpression "{0} field must contain only letters, numbers or | . | - | _ | characters.
That's it..
Given the following objects:
public class Customer {
public String Name { get; set; }
public String Address { get; set; }
}
public class Invoice {
public String ID { get; set; }
public DateTime Date { get; set; }
public Customer BillTo { get; set; }
}
I'd like to use reflection to go through the Invoice to get the Name property of a Customer. Here's what I'm after, assuming this code would work:
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);
Of course, this fails since "BillTo.Address" is not a valid property of the Invoice class.
So, I tried writing a method to split the string into pieces on the period, and walk the objects looking for the final value I was interested in. It works okay, but I'm not entirely comfortable with it:
public Object GetPropValue(String name, Object obj) {
foreach (String part in name.Split('.')) {
if (obj == null) { return null; }
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
Any ideas on how to improve this method, or a better way to solve this problem?
EDIT after posting, I saw a few related posts... There doesn't seem to be an answer that specifically addresses this question, however. Also, I'd still like the feedback on my implementation.
I use following method to get the values from (nested classes) properties like
"Property"
"Address.Street"
"Address.Country.Name"
public static object GetPropertyValue(object src, string propName)
{
if (src == null) throw new ArgumentException("Value cannot be null.", "src");
if (propName == null) throw new ArgumentException("Value cannot be null.", "propName");
if(propName.Contains("."))//complex type nested
{
var temp = propName.Split(new char[] { '.' }, 2);
return GetPropertyValue(GetPropertyValue(src, temp[0]), temp[1]);
}
else
{
var prop = src.GetType().GetProperty(propName);
return prop != null ? prop.GetValue(src, null) : null;
}
}
Here is the Fiddle: https://dotnetfiddle.net/PvKRH0
I know I'm a bit late to the party, and as others said, your implementation is fine
...for simple use cases.
However, I've developed a library that solves exactly that use case, Pather.CSharp.
It is also available as Nuget Package.
Its main class is Resolver with its Resolve method.
You pass it an object and the property path, and it will return the desired value.
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Address");
But it can also resolve more complex property paths, including array and dictionary access.
So, for example, if your Customer had multiple addresses
public class Customer {
public String Name { get; set; }
public IEnumerable<String> Addresses { get; set; }
}
you could access the second one using Addresses[1].
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Addresses[1]");
I actually think your logic is fine. Personally, I would probably change it around so you pass the object as the first parameter (which is more inline with PropertyInfo.GetValue, so less surprising).
I also would probably call it something more like GetNestedPropertyValue, to make it obvious that it searches down the property stack.
You have to access the ACTUAL object that you need to use reflection on. Here is what I mean:
Instead of this:
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);
Do this (edited based on comment):
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo");
Customer cust = (Customer)info.GetValue(inv, null);
PropertyInfo info2 = cust.GetType().GetProperty("Address");
Object val = info2.GetValue(cust, null);
Look at this post for more information:
Using reflection to set a property of a property of an object
In hopes of not sounding too late to the party, I would like to add my solution:
Definitely use recursion in this situation
public static Object GetPropValue(String name, object obj, Type type)
{
var parts = name.Split('.').ToList();
var currentPart = parts[0];
PropertyInfo info = type.GetProperty(currentPart);
if (info == null) { return null; }
if (name.IndexOf(".") > -1)
{
parts.Remove(currentPart);
return GetPropValue(String.Join(".", parts), info.GetValue(obj, null), info.PropertyType);
} else
{
return info.GetValue(obj, null).ToString();
}
}
You don't explain the source of your "discomfort," but your code basically looks sound to me.
The only thing I'd question is the error handling. You return null if the code tries to traverse through a null reference or if the property name doesn't exist. This hides errors: it's hard to know whether it returned null because there's no BillTo customer, or because you misspelled it "BilTo.Address"... or because there is a BillTo customer, and its Address is null! I'd let the method crash and burn in these cases -- just let the exception escape (or maybe wrap it in a friendlier one).
Here is another implementation that will skip a nested property if it is an enumerator and continue deeper. Properties of type string are not affected by the Enumeration Check.
public static class ReflectionMethods
{
public static bool IsNonStringEnumerable(this PropertyInfo pi)
{
return pi != null && pi.PropertyType.IsNonStringEnumerable();
}
public static bool IsNonStringEnumerable(this object instance)
{
return instance != null && instance.GetType().IsNonStringEnumerable();
}
public static bool IsNonStringEnumerable(this Type type)
{
if (type == null || type == typeof(string))
return false;
return typeof(IEnumerable).IsAssignableFrom(type);
}
public static Object GetPropValue(String name, Object obj)
{
foreach (String part in name.Split('.'))
{
if (obj == null) { return null; }
if (obj.IsNonStringEnumerable())
{
var toEnumerable = (IEnumerable)obj;
var iterator = toEnumerable.GetEnumerator();
if (!iterator.MoveNext())
{
return null;
}
obj = iterator.Current;
}
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
}
based on this question and on
How to know if a PropertyInfo is a collection
by Berryl
I use this in a MVC project to dynamically Order my data by simply passing the Property to sort by
Example:
result = result.OrderBy((s) =>
{
return ReflectionMethods.GetPropValue("BookingItems.EventId", s);
}).ToList();
where BookingItems is a list of objects.
> Get Nest properties e.g., Developer.Project.Name
private static System.Reflection.PropertyInfo GetProperty(object t, string PropertName)
{
if (t.GetType().GetProperties().Count(p => p.Name == PropertName.Split('.')[0]) == 0)
throw new ArgumentNullException(string.Format("Property {0}, is not exists in object {1}", PropertName, t.ToString()));
if (PropertName.Split('.').Length == 1)
return t.GetType().GetProperty(PropertName);
else
return GetProperty(t.GetType().GetProperty(PropertName.Split('.')[0]).GetValue(t, null), PropertName.Split('.')[1]);
}
if (info == null) { /* throw exception instead*/ }
I would actually throw an exception if they request a property that doesn't exist. The way you have it coded, if I call GetPropValue and it returns null, I don't know if that means the property didn't exist, or the property did exist but it's value was null.
public static string GetObjectPropertyValue(object obj, string propertyName)
{
bool propertyHasDot = propertyName.IndexOf(".") > -1;
string firstPartBeforeDot;
string nextParts = "";
if (!propertyHasDot)
firstPartBeforeDot = propertyName.ToLower();
else
{
firstPartBeforeDot = propertyName.Substring(0, propertyName.IndexOf(".")).ToLower();
nextParts = propertyName.Substring(propertyName.IndexOf(".") + 1);
}
foreach (var property in obj.GetType().GetProperties())
if (property.Name.ToLower() == firstPartBeforeDot)
if (!propertyHasDot)
if (property.GetValue(obj, null) != null)
return property.GetValue(obj, null).ToString();
else
return DefaultValue(property.GetValue(obj, null), propertyName).ToString();
else
return GetObjectPropertyValue(property.GetValue(obj, null), nextParts);
throw new Exception("Property '" + propertyName.ToString() + "' not found in object '" + obj.ToString() + "'");
}
I wanted to share my solution although it may be too late. This solution is primarily to check if the nested property exists. But it can be easily tweaked to return the property value if needed.
private static PropertyInfo _GetPropertyInfo(Type type, string propertyName)
{
//***
//*** Check if the property name is a complex nested type
//***
if (propertyName.Contains("."))
{
//***
//*** Get the first property name of the complex type
//***
var tempPropertyName = propertyName.Split(".", 2);
//***
//*** Check if the property exists in the type
//***
var prop = _GetPropertyInfo(type, tempPropertyName[0]);
if (prop != null)
{
//***
//*** Drill down to check if the nested property exists in the complex type
//***
return _GetPropertyInfo(prop.PropertyType, tempPropertyName[1]);
}
else
{
return null;
}
}
else
{
return type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
}
}
I had to refer to few posts to come up with this solution. I think this will work for multiple nested property types.
My internet connection was down when I need to solve the same problem, so I had to 're-invent the wheel':
static object GetPropertyValue(Object fromObject, string propertyName)
{
Type objectType = fromObject.GetType();
PropertyInfo propInfo = objectType.GetProperty(propertyName);
if (propInfo == null && propertyName.Contains('.'))
{
string firstProp = propertyName.Substring(0, propertyName.IndexOf('.'));
propInfo = objectType.GetProperty(firstProp);
if (propInfo == null)//property name is invalid
{
throw new ArgumentException(String.Format("Property {0} is not a valid property of {1}.", firstProp, fromObject.GetType().ToString()));
}
return GetPropertyValue(propInfo.GetValue(fromObject, null), propertyName.Substring(propertyName.IndexOf('.') + 1));
}
else
{
return propInfo.GetValue(fromObject, null);
}
}
Pretty sure this solves the problem for any string you use for property name, regardless of extent of nesting, as long as everything's a property.
Based on the original code from #jheddings, I have created a extension method version with generic type and verifications:
public static T GetPropertyValue<T>(this object sourceObject, string propertyName)
{
if (sourceObject == null) throw new ArgumentNullException(nameof(sourceObject));
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException(nameof(propertyName));
foreach (string currentPropertyName in propertyName.Split('.'))
{
if (string.IsNullOrWhiteSpace(currentPropertyName)) throw new InvalidOperationException($"Invalid property '{propertyName}'");
PropertyInfo propertyInfo = sourceObject.GetType().GetProperty(currentPropertyName);
if (propertyInfo == null) throw new InvalidOperationException($"Property '{currentPropertyName}' not found");
sourceObject = propertyInfo.GetValue(sourceObject);
}
return sourceObject is T result ? result : default;
}
I wrote a method that received one object type as the argument from the input and returns dictionary<string,string>
public static Dictionary<string, string> GetProperties(Type placeHolderType)
{
var result = new Dictionary<string, string>();
var properties = placeHolderType.GetProperties();
foreach (var propertyInfo in properties)
{
string name = propertyInfo.Name;
string description = GetDescriptionTitle(propertyInfo);
if (IsNonString(propertyInfo.PropertyType))
{
var list = GetProperties(propertyInfo.PropertyType);
foreach (var item in list)
{
result.Add($"{propertyInfo.PropertyType.Name}_{item.Key}", item.Value);
}
}
else
{
result.Add(name, description);
}
}
return result;
}
public static bool IsNonString(Type type)
{
if (type == null || type == typeof(string))
return false;
return typeof(IPlaceHolder).IsAssignableFrom(type);
}
private static string GetDescriptionTitle(MemberInfo memberInfo)
{
if (Attribute.GetCustomAttribute(memberInfo, typeof(DescriptionAttribute)) is DescriptionAttribute descriptionAttribute)
{
return descriptionAttribute.Description;
}
return memberInfo.Name;
}
public static object GetPropertyValue(object src, string propName)
{
if (src == null) throw new ArgumentException("Value cannot be null.", "src");
if (propName == null) throw new ArgumentException("Value cannot be null.", "propName");
var prop = src.GetType().GetProperty(propName);
if (prop != null)
{
return prop.GetValue(src, null);
}
else
{
var props = src.GetType().GetProperties();
foreach (var property in props)
{
var propInfo = src.GetType().GetProperty(property.Name);
if (propInfo != null)
{
var propVal = propInfo.GetValue(src, null);
if (src.GetType().GetProperty(property.Name).PropertyType.IsClass)
{
return GetPropertyValue(propVal, propName);
}
return propVal;
}
}
return null;
}
usage: calling part
var emp = new Employee() { Person = new Person() { FirstName = "Ashwani" } };
var val = GetPropertyValue(emp, "FirstName");
above can search the property value at any level
Try inv.GetType().GetProperty("BillTo+Address");