using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Stylet.Samples.ModelValidation { /// /// Type which can be converter to and from both a T and a string. Useful for binding a TextBox to, say, an int. /// /// /// Stringable{T} has two value properties - StringValue, which is always set, and Value, which is set if Value can be converted to a T. /// If you create a new Stringable{T} from a T, both Value and StringValue are set. If you Create one from a string (using Stringable{T}.FromString) /// then StringValue is set, and Value is set if that string value can be converted to a T> /// IsValid indicates whether Value could be set. /// /// This type has an associated TypeConverter, StringableConverter, which will be used by WPF to convert this Stringable{T} to and from a string. /// // I would make this a struct, but it will have to be boxed to go through the TypeConverter, so there's no point [TypeConverter(typeof(StringableConverter))] public class Stringable : IEquatable> { private readonly string _stringValue; /// /// String representation of the value /// public string StringValue { get { return this._stringValue; } } private readonly T _value; /// /// Actual value, or default(T) if IsValid is false /// public T Value { get { return this._value; } } private readonly bool _isValid; /// /// True if Value ias a proper value (i.e. we were constructed from a T, or we were constructed from a string which could be converted to a T) /// public bool IsValid { get { return this._isValid; } } /// /// Create a new instance, representing the given value /// /// Value to represent public Stringable(T value) : this(value, value.ToString(), true) { } private Stringable(T value, string stringValue, bool isValid) { this._value = value; this._stringValue = stringValue; this._isValid = isValid; } /// /// Create a new instance from the given string. If the string can be converted to a T, then IsValue is true and Value contains the converted value. /// If not, IsValid is false and Value is default(T) /// /// /// public static Stringable FromString(string stringValue) { T dest = default(T); bool isValid = false; // The TypeConverter for String can't convert it into anything else, so don't bother getting that var fromConverter = TypeDescriptor.GetConverter(typeof(T)); if (fromConverter.CanConvertFrom(typeof(string)) && fromConverter.IsValid(stringValue)) { dest = (T)fromConverter.ConvertFrom(stringValue); isValid = true; } return new Stringable(dest, stringValue, isValid); } public static implicit operator T(Stringable stringable) { return stringable.Value; } public static implicit operator Stringable(T value) { return new Stringable(value); } public override string ToString() { return this.StringValue; } public override bool Equals(object obj) { return base.Equals(obj as Stringable); } public bool Equals(Stringable other) { return other != null && EqualityComparer.Default.Equals(this.Value, other.Value) && this.StringValue == other.StringValue; } public static bool operator ==(Stringable o1, Stringable o2) { if (Object.ReferenceEquals(o1, o2)) return true; if ((object)o1 == null || (object)o2 == null) return false; return o1.Equals(o2); } public static bool operator !=(Stringable o1, Stringable o2) { return !(o1 == o2); } public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 27 + this.Value.GetHashCode(); hash = hash * 27 + this.StringValue.GetHashCode(); return hash; } } } /// /// TypeConverter for Stringable{T} /// /// /// This is used by WPF. This means that if a Stringable{T} property is bound to a string control (e.g. TextBox), then this TypeConverter /// is used to convert from that string back to a Stringable{T} /// public class StringableConverter : TypeConverter { private readonly Type valueType; private readonly Func generator; public StringableConverter(Type type) { if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Stringable<>) || type.GetGenericArguments().Length != 1) throw new ArgumentException("Incompatible type", "type"); this.valueType = type; // Generate a Func which gives us a Stringable, given a string // WPF instantiates us once, then uses us lots, so the overhead of doing this here is worth it var fromMethod = type.GetMethod("FromString", BindingFlags.Static | BindingFlags.Public); var param = Expression.Parameter(typeof(string)); this.generator = Expression.Lambda>(Expression.Call(fromMethod, param), param).Compile(); } public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { return this.generator(value as string); } public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { return destinationType == typeof(string) || destinationType == this.valueType || base.CanConvertTo(context, destinationType); } public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { if (value == null) return String.Empty; // Common case = just call the overloaded ToString - no need for reflection if (destinationType == typeof(string)) return value.ToString(); var valueType = value.GetType(); if (destinationType.IsAssignableFrom(this.valueType) && typeof(Stringable<>).IsAssignableFrom(valueType)) { var valueProperty = valueType.GetProperty("Value", BindingFlags.Public | BindingFlags.Instance); return valueProperty.GetValue(value); } return base.ConvertTo(context, culture, value, destinationType); } } }