How do I bind to a custom silverlight control?

2019-09-07 06:04发布

问题:

I am having problems binding to my control. I would like the label(lblLabel) in my control to display the metadata from whatever is bound to the Field Property. It currently displays "Field" as a label. How do I get it to display "Customer Name :" which is the Name on the view model for property, CustomerName?

My Controls XAML

<UserControl x:Name="ctlRowItem" x:Class="ApplicationShell.Controls.RowItem"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
    xmlns:my="clr-namespace:SilverlightApplicationCore.Controls;assembly=SilverlightApplicationCore"
    xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="g_required" Width="15" />
            <ColumnDefinition x:Name="g_label" Width="200" />
            <ColumnDefinition x:Name="g_control" Width="auto" />
            <ColumnDefinition x:Name="g_fieldEnd" Width="*" />
        </Grid.ColumnDefinitions>

        <sdk:Label x:Name="lblRequired" Grid.Column="0" Grid.Row="0" />
        <sdk:Label x:Name="lblLabel" Grid.Column="1" Grid.Row="3" Target="{Binding ElementName=txtControl}" PropertyPath="Field" />

        <TextBox x:Name="txtControl" Grid.Column="2" Grid.Row="3" MaxLength="10" Width="150" Text="{Binding Field, Mode=TwoWay, ElementName=ctlRowItem}" />     
    </Grid>
</UserControl>

My Controls CODE BEHIND

using System.Windows;<BR>
using System.Windows.Controls;<BR>
using System.Windows.Data;<BR>
using ApplicationShell.Resources;<BR>

namespace ApplicationShell.Controls
{
    public partial class RowItem : UserControl
    {

        #region Properties

        public object Field
        {
            get { return (string)GetValue(FieldProperty); }
            set { SetValue(FieldProperty, value); }
        }

        #region Dependency Properties

        public static readonly DependencyProperty FieldProperty = DependencyProperty.Register("Field", typeof(object), typeof(RowItem), new PropertyMetadata(null, Field_PropertyChangedCallback));

        #endregion

        #endregion

        #region Events

        #region Dependency Properties

        private static void Field_PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.OldValue != e.NewValue)
                return;

            var control = (RowItem)d;
            control.Field = (object)e.NewValue;
        }

        #endregion

        #endregion

        #region Constructor

        public RowItem()
        {
            InitializeComponent();
        }

        #endregion

    }
}

View Model

namespace ApplicationShell.Web.ViewModel
{
    [Serializable]
    public class Customers
    {
        [Display(AutoGenerateField = false, ShortName="CustomerName_Short", Name="CustomerName_Long", ResourceType = typeof(LocaleLibrary))]
        public override string CustomerName { get; set; }
    }
}

XAML which calls the My Control

This pages datacontext is set to a property of type Customers (View Model).

<controls:ChildWindow x:Class="ApplicationShell.CustomerWindow"
           xmlns:my="clr-namespace:SilverlightApplicationCore.Controls;assembly=SilverlightApplicationCore"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
           Title="Customer View">

<my:RowItem x:name="test" Field="{Binding CustomerName,Mode=TwoWay}" />
</controls:ChildWindow>

回答1:

There is a way of getting at the display names of the properties bound to, but sadly it is not trivial and we have to make assumptions about the property-paths used.

I'm aware that the Silverlight Toolkit ValidationSummary is able to find out property names of bindings automatically, but when I looked through its source code, I found that it does this by doing its own evaluation of the binding path.

So, that's the approach I'll take here.

I modified the code-behind of your RowItem user-control, and this is what I came up with:

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

public partial class RowItem : UserControl
{
    public RowItem()
    {
        InitializeComponent();
        Dispatcher.BeginInvoke(SetFieldLabel);
    }

    public string Field
    {
        get { return (string)GetValue(FieldProperty); }
        set { SetValue(FieldProperty, value); }
    }

    public static readonly DependencyProperty FieldProperty =
        DependencyProperty.Register("Field", typeof(string), typeof(RowItem),
                                    null);

    /// <summary>
    /// Return the display name of the property at the end of the given binding
    /// path from the given source object.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The display name of the property is the name of the property according
    /// to a <see cref="DisplayAttribute"/> set on the property, if such an
    /// attribute is found, otherwise the name of the property.
    /// </para>
    /// <para>
    /// This method supports dot-separated binding paths only.  Binding
    /// expressions such <c>[0]</c> or <c>(...)</c> are not supported and will
    /// cause this method to return null.
    /// </para>
    /// <para>
    /// If no suitable property could be found (due to an intermediate value
    /// of the property-path evaluating to <c>null</c>, or no property with a
    /// given name being found), <c>null</c> is returned.  The final property
    /// in the path can have a <c>null</c> value, as that value is never used.
    /// </para>
    /// </remarks>
    /// <param name="binding">The binding expression.</param>
    /// <param name="source">
    /// The source object at which to start the evaluation.
    /// </param>
    /// <returns>
    /// The display name of the property at the end of the binding, or
    /// <c>null</c> if this could not be determined.
    /// </returns>
    private string GetBindingPropertyDisplayName(BindingExpression binding,
                                                 object source)
    {
        if (binding == null)
        {
            throw new ArgumentNullException("binding");
        }

        string bindingPath = binding.ParentBinding.Path.Path;
        object obj = source;
        PropertyInfo propInfo = null;
        foreach (string propertyName in bindingPath.Split('.'))
        {
            if (obj == null)
            {
                // Null object not at the end of the path.
                return null;
            }

            Type type = obj.GetType();
            propInfo = type.GetProperty(propertyName);
            if (propInfo == null)
            {
                // No property with the given name.
                return null;
            }

            obj = propInfo.GetValue(obj, null);
        }

        DisplayAttribute displayAttr = 
            propInfo.GetCustomAttributes(typeof(DisplayAttribute), false)
            .OfType<DisplayAttribute>()
            .FirstOrDefault();

        if (displayAttr != null)
        {
            return displayAttr.GetName();
        }
        else
        {
            return propInfo.Name;
        }
    }

    private void SetFieldLabel()
    {
        BindingExpression binding = this.GetBindingExpression(FieldProperty);
        string displayName = GetBindingPropertyDisplayName(binding,
                                                           DataContext);
        if (lblLabel != null)
        {
            lblLabel.Content = displayName;
        }
    }
}

There are a few things to note:

  • To use this code, your project will need a reference to System.ComponentModel.DataAnnotations. However, that shouldn't be a problem as that's the same reference you need in order to use the Display attribute.

  • The function SetFieldLabel is called to set the label of the field. I found that the most reliable place to call it was from Dispatcher.BeginInvoke. Calling this method directly from within the constructor or from within a Loaded event handler did not work, as the binding had not been set up by then.

  • Only binding paths consisting of a dot-separated list of property names are supported. Something like SomeProp.SomeOtherProp.YetAnotherProp are fine, but SomeProp.SomeList[0] is not supported and will not work. If the display name of the binding property cannot be determined, nothing will be displayed.

  • There's no longer a PropertyChangedCallback on the Field dependency property. We're not really interested in what happens whenever the user changes the text in the control. It's not going to change the display name of the property bound to.

For test purposes, I knocked up the following view-model class:

public class ViewModel
{
    // INotifyPropertyChanged implementation omitted.

    [Display(Name = "This value is in a Display attribute")]
    public string WithDisplay { get; set; }

    public string WithoutDisplay { get; set; }

    [Display(Name = "ExampleFieldNameKey", ResourceType = typeof(Strings))]
    public string Localised { get; set; }

    public object This { get { return this; } }

    public object TheVerySame { get { return this; } }
}

(The Resources collection Strings.resx contains a single key, with name ExampleFieldNameKey and value This value is in a Resources.resx. This collection also has its Access Modifier set to Public.) I tested out my modifications to your control using the following XAML, with the DataContext set to an instance of the view-model class presented above:

<StackPanel>
    <local:RowItem Field="{Binding Path=WithDisplay, Mode=TwoWay}" />
    <local:RowItem Field="{Binding Path=WithoutDisplay, Mode=TwoWay}" />
    <local:RowItem Field="{Binding Path=Localised, Mode=TwoWay}" />
    <local:RowItem Field="{Binding Path=This.This.TheVerySame.This.WithDisplay, Mode=TwoWay}" />
</StackPanel>

This gave me four RowItems, with the following labels:

This value is in a Display attribute
WithoutDisplay
This value is in a Resources.resx
This value is in a Display attribute