I'm using the following code to set Control
properties in a thread-safe manner:
private delegate void SetPropertyThreadSafeDelegate<TPropertyType>(Control @this, Expression<Func<TPropertyType>> property, TPropertyType value);
public static void SetPropertyThreadSafe<TPropertyType>(this Control @this, Expression<Func<TPropertyType>> property, TPropertyType value)
{
var propertyInfo = (property.Body as MemberExpression ?? (property.Body as UnaryExpression).Operand as MemberExpression).Member as PropertyInfo;
if (propertyInfo == null ||
!propertyInfo.ReflectedType.IsAssignableFrom(@this.GetType()) ||
@this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
{
throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
}
if (propertyInfo.PropertyType.IsValueType &&
!propertyInfo.PropertyType.IsAssignableFrom(typeof(TPropertyType)))
{
throw new ArgumentException(string.Format("Attempted to assign incompatible value type: expecting {0}, got {1}.", propertyInfo.PropertyType, typeof(TPropertyType)));
}
if (@this.InvokeRequired)
{
@this.Invoke(new SetPropertyThreadSafeDelegate<TPropertyType>(SetPropertyThreadSafe), new object[] { @this, property, value });
}
else
{
@this.GetType().InvokeMember(propertyInfo.Name, BindingFlags.SetProperty, null, @this, new object[] { value });
}
}
It's called like so:
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 32);
The reason for doing this is to get compile-time checking of property names and type assignments. It works perfectly for standard objects, but everything goes a bit pear-shaped with value types because the compiler is happy to accept the following, which of course bombs at runtime:
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 'c');
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, long.MaxValue);
I've already modified the SetPropertyThreadSafe
method to handle the case when value types are used, and throw an exception if the incorrect type is used as an argument, but what I'm really loooking for is the ability to get this method to perform compile-time type checking for 100% of cases, i.e. objects and value types. Is this even possible and if so how would I need to modify my code to do this?
Change the contract to:
public static void SetPropertyThreadSafe<TPropertyType, TValue>(
this Control self,
Expression<Func<TPropertyType>> property,
TValue value)
where TValue : TPropertyType
Note that with this you no longer need to do the IsAssignableFrom check since the compiler will enforce it.
The reasons your example compiled is because the compiler made a guess as to what the type parameter was. Here is what the compiler turns those calls into:
progBar.SetPropertyThreadSafe<int>(() => progBar.Step, 'c');
progBar.SetPropertyThreadSafe<long>(() => progBar.Step, long.MaxValue);
Notice how the first one is int, that's because ProgressBar.Step is an int and 'c' is a char which has an implicit conversion to int. Same with the next example, int has an implicit conversion to long, and the second one is long, so the compiler guesses that it is long.
If you want inheritance and conversions like those to work, don't make the compiler guess. Your two solutions are:
Of course this is less than ideal, because then you are basically hard coding in the type of the Func. What you really want to do is let the compiler determine both types and tell you if they are compatible.
NOTE: Below is the code that I would use, which is entirely different from yours:
public static void SetPropertyThreadSafe<TControl>(this TControl self, Action<TControl> setter)
where TControl : Control
{
if (self.InvokeRequired)
{
var invoker = (Action)(() => setter(self));
self.Invoke(invoker);
}
else
{
setter(self);
}
}
public static void Example()
{
var progBar = new ProgressBar();
progBar.SetPropertyThreadSafe(p => p.Step = 3);
}
You just need to make some minor changes to your generic and expression:
public static void SetPropertyThreadSafe<TSource, TPropertyType>(this TSource source, Expression<Func<TSource, TPropertyType>> property, TPropertyType value)
Then you supply a lambda like so:
var someObject = new /*Your Object*/
someObject.SetPropertyThreadSafe(x => x.SomeProperty, /* Your Value */);
The value you specify must be covariant to the type of SomeProperty, and this is checked at compile time. Let me know if I'm misunderstanding something. If you need to constrain it to Control, you simply change the signature to
this Control source
or
where TSource : Control