Solved reading this article
The problem of null source was related to the fact that Markup Extension are evaluated only once at parse time, so the View didn't have its DataContext at that moment. The solution is to subscribe to an event that raises when the value is ready to be read by the extension.
This is my solution to bind to a property attribute:
usage:
<TextBlock Text="{local:DisplayDescription Binding={Binding PropertyName}}" />
code:
public abstract class UpdatableMarkupExtension : MarkupExtension
{
protected object TargetObject { get; private set; }
protected object TargetProperty { get; private set; }
public sealed override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (target != null)
{
this.TargetObject = target.TargetObject;
this.TargetProperty = target.TargetProperty;
}
this.Subscribe();
return ProvideValueInternal(serviceProvider);
}
protected void UpdateValue(object value)
{
if (this.TargetObject != null)
{
if (this.TargetProperty is DependencyProperty)
{
DependencyObject obj = this.TargetObject as DependencyObject;
DependencyProperty prop = this.TargetProperty as DependencyProperty;
Action updateAction = () => obj.SetValue(prop, value);
if (obj.CheckAccess())
updateAction();
else
obj.Dispatcher.Invoke(updateAction);
}
else
{
PropertyInfo prop = this.TargetProperty as PropertyInfo;
prop.SetValue(this.TargetObject, value, null);
}
}
}
protected abstract void Subscribe();
protected abstract object ProvideValueInternal(IServiceProvider serviceProvider);
}
[MarkupExtensionReturnType(typeof(string))]
public class DisplayDescriptionExtension : UpdatableMarkupExtension
{
public DisplayDescriptionExtension()
{
}
public DisplayDescriptionExtension(Binding binding)
{
this.Binding = binding;
}
[ConstructorArgument("binding")]
public Binding Binding { get; set; }
void DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var pi = (sender as FrameworkElement).DataContext.GetType().GetProperty(this.Binding.Path.Path);
var displayAtt = pi.GetCustomAttribute<DisplayAttribute>(true);
var displayName = displayAtt != null ? displayAtt.Description : string.Empty;
this.UpdateValue(displayName);
}
protected override object ProvideValueInternal(IServiceProvider serviceProvider)
{
return "!";
}
protected override void Subscribe()
{
(this.TargetObject as FrameworkElement).DataContextChanged += DataContextChanged;
}
}
The use of the binding ensures it refers to a real property so I can notice any typos at design time.
The PropertyPath logic still has to be extended, atm it works only for first level properties.
Many thanks to Willem van Rumpt
for his time and ideas.