So I have a PropertyBag class that is intended to implement INotifyPropertyChanged. In order to make this code work as cleanly as possible and to avoid user error, I am using the stack to get the property name. See, if the property name doesn't match the actual property exactly, then you will have a failure and I am trying to protect from that.
So, here is an example usage of the class:
public class MyData : PropertyBag
{
public MyData()
{
Foo = -1;
}
public int Foo
{
get { return GetProperty<int>(); }
set { SetProperty(value); }
}
}
The important code for the base PropertyBag is here:
public abstract class PropertyBag : INotifyPropertyChanged
{
protected T GetProperty<T>()
{
string propertyName = PropertyName((new StackTrace()).GetFrame(1));
if (propertyName == null)
throw new ArgumentException("GetProperty must be called from a property");
return GetValue<T>(propertyName);
}
protected void SetProperty<T>(T value)
{
string propertyName = PropertyName((new StackTrace()).GetFrame(1));
if (propertyName == null)
throw new ArgumentException("SetProperty must be called from a property");
SetValue(propertyName, value);
}
private static string PropertyName(StackFrame frame)
{
if (frame == null) return null;
if (!frame.GetMethod().Name.StartsWith("get_") &&
!frame.GetMethod().Name.StartsWith("set_"))
return null;
return frame.GetMethod().Name.Substring(4);
}
}
So now that you have seen my code, I can tell you the problem... In some cases under release build, the "Foo" setter in the "MyData" constructor appears to be getting optimized to inline as SetProperty(-1). Unfortunately, this inline optimization fails out my SetProperty method because I am no longer calling it from a property! FAIL. It appears that I cannot rely on the StackTrace in this way.
Can anyone
A: Figure out a better way to do this but still avoid passing in "Foo" to GetProperty and SetProperty?
B: Figure out a way to tell the compiler to not optimize in this case?
Using the stack here is slow and unnecessary; I would simply use:
get { return GetProperty<int>("Foo"); }
set { SetProperty("Foo", value); }
(hint: I've done a lot of work with custom property models; I know that this works well...)
Another alternative is an object key (use reference equality to compare) - a lot of ComponentModel
works this way, as do some of the properties in WF/WPF:
static readonly object FooKey = new object();
...
get { return GetProperty<int>(FooKey); }
set { SetProperty(FooKey, value); }
Of course, you could declare a type for the keys (with a Name
property), and use that:
static readonly PropertyKey FooKey = new PropertyKey("Foo");
etc; however, to answer the question: mark it (but don't do this) with:
[MethodImpl(MethodImplOptions.NoInlining)]
or
[MethodImpl(MethodImplOptions.NoOptimization)]
or
[MethodImpl(MethodImplAttributes.NoOptimization
| MethodImplAttributes.NoInlining)]
Using the stack is not a good idea. You are relying on internal implementation of the compiler to artificially tie in your property bag to the language properties.
MethodImpl
attribute makes the use of your property bag non-transparent for other developers.MethodImpl
attribute, nothing guarantees you it will be the first frame on the call stack. It is possible that the assembly was instrumented or modified to inject calls between the actual property and the call to your property bag. (Think aspect programming)'_get'
and '_set'
You should really just implement your property bag accessors to take a parameter to identify the property - either a string name (like Hastable) or an object (like the WPF dependency property bag)
Try the new [CallerMemberName] attribute.
Place it on a parameter to your method ([CallerMemberName] callerName = null) and the compiler will rewrite all calls to your method to pass the caller name automatically (your calls don't pass the parameter at all).
It doesn't eliminate any optimizations, and is much faster than lambdas or reflection or stacks, and works in Release mode.
P.S. if CallerMemberNameAttribute doesn't exist in your version of the framework, just define it (empty). It's a language feature, not a framework feature. When the compiler sees [CallerMemberNameAttribute] on a parameter, it just works.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With