I'm implementing a custom localization system where I make use of property attributes to store the localized text. What I need to do is set a Textblock's Text with the text contained in the DisplayName attribute. For example I have this property:

[DisplayName("First name")] public string FirstName { get; set; }

and in XAML I need to do something like

<Textblock Text={mvvm:Localize FirstName} />

or

<Textblock Text={Binding FirstName, Converter={Staticresource DisplayNameReader}} />

but I can't find a way to reach the attribute, since the converter only knows value of the property, type of the property's exposing class, and a parameter.

I tried with an extension method, doing something like

<Textblock Text={mvvm:Localize {Binding FirstName}} />

in order to pass a binding to the property, but all I get is a binding with a null source.

Can u help me please? Thank you guys!

EDIT: this is my markup extension

public class DisplayDescriptionExtension : MarkupExtension
{
    public DisplayDescriptionExtension() { }

    public DisplayDescriptionExtension(Binding binding)
    {
        this.Binding = binding;
    }

    [ConstructorArgument("binding")]
    public Binding Binding { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (this.Binding == null ||
            this.Binding.Source == null)
            return string.Empty;

        var propertyInfo = piSource.GetType().GetProperty(step);

        var displayAtts = Attribute.GetCustomAttributes(propertyInfo, typeof(DisplayAttribute), true);

        if (displayAtts != null)
            return (displayAtts[0] as DisplayAttribute).Description;

        return string.Empty;
    }
}

the problem is that the first if is always satisfied since the source is null.

有帮助吗?

解决方案

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.

其他提示

Expanding on your MarkupExtension, the code samples below should work for you. You can specify a property path and either a type or an object for the markup extension, and from there, you can find the member you're interested in.

EDIT:

I've kept the original solution (striked through) below the new one, should you want to reference it. There's also an option to use a similar approach as Binding extensions, but it's more complicated, relies on reflection to create a BindingExpression, you'd still need to use DependencyObjects, is less clear, and, IMHO, not worth the trouble. Also, I've don't have a working example yet ;).

It might perhaps be interesting to investigate that route if you're allowing for runtime switching of languages, in which case having an actual binding might be worth your while.

public class DisplayDescriptionExtension : MarkupExtension
{
    public DisplayDescriptionExtension() { }
    public DisplayDescriptionExtension(string propertyPath)
    {
        PropertyPath = propertyPath;
    }

    [ConstructorArgument("propertyPath")]
    public string PropertyPath { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
        {
            return null;
        }

        if (string.IsNullOrEmpty(PropertyPath))
        {
            return null;
        }

        IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (pvt == null)
        {
            return null;
        }

        FrameworkElement d = pvt.TargetObject as FrameworkElement;
        if (d == null)
        {
            return null;
        }

        object context = d.DataContext;
        if (context == null)
        {
            return null;
        }

        object returnValue = null;
        ///* ToDo:
        // * Using "PropertyPath" and context, find the member you're interested in, and 
        // * fill returnValue approriately. Keep in mind that "PropertyPath" may contain nested properties! 
        // */

        return returnValue;
    }
}

XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApplication1"
        Title="MainWindow"
        Height="350"
        Width="525"
        >
    <Window.DataContext>
        <l:ViewModel x:Name="TheViewModel" />
    </Window.DataContext>
    <Grid x:Name="Root">
        <Button Content="{l:DisplayDescription Value}" />
    </Grid>
</Window>

OLD SOLUTION

public class DisplayDescriptionExtension : MarkupExtension { public DisplayDescriptionExtension() { }

    public DisplayDescriptionExtension(string propertyPath, object context)
    {
        PropertyPath = propertyPath;
        Context = context;
    }

    public DisplayDescriptionExtension(string propertyPath, Type contextType)
    {
        PropertyPath = propertyPath;
        ContextType = contextType;
    }

    [ConstructorArgument("propertyPath")]
    public string PropertyPath { get; set; }

    [ConstructorArgument("context")]
    public object Context { get; set; }

    [ConstructorArgument("contextType")]
    public Type ContextType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
        {
            return null;
        }

        if (string.IsNullOrEmpty(PropertyPath))
        {
            return null;
        }

        Type contextType = ContextType;
        if (contextType == null)
        {
            if (Context == null)
            {
                return null;
            }

            contextType = Context.GetType();
        }

        object returnValue = null;

        /* ToDo:
         * Using "PropertyPath" and contextType, find the member you're interested in, and 
         * fill returnValue approriately. Keep in mind that "PropertyPath" may contain nested properties! 
         */

        return returnValue;
    }
}

usage:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApplication1"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Window.DataContext>
        <l:ViewModel x:Name="TheViewModel" />
    </Window.DataContext>
    <Grid>
        <Grid.Resources>
            <l:ValueConverter x:Key="converter" />
        </Grid.Resources>
        <!-- Using an object reference -->
        <Button Content="{l:DisplayDescriptionExtension MyProperty.NestedProperty, {x:Reference Name=TheViewModel}}" />
        <!-- Using a Type -->
        <Button Content="{l:DisplayDescriptionExtension OtherProperty, {x:Type l:ViewModel}}" />
    </Grid>
</Window>

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top