Bob Sammers
Bob Sammers

Reputation: 3270

Binding Errors on partially constructed objects

I have created some attached properties to allow indirection of a binding (by which I mean binding to a value whose name is given by the attached property, rather than specified as a literal in XAML).

Some of the AP's are optional (for example one that overrides the DataContext that would otherwise be in force) and this means that I am attempting to create the binding when not all the APs have been set (because in the PropertyChangedCallback I don't know whether the others will be set).

The upshot is that the binding can be created multiple times, sometimes unsuccessfully and this results in binding errors which are "unsightly", for want of a better word.

Is there a way of suppressing binding errors until all the APs of an element have been assigned, or working out from within the PropertyChangedCallback whether any more of the containing class's AP's will be set on this element?

Edit

I've been asked for code. I was hoping to do this without (because it's made the question rather long!), but here is the class I am asking about:

public static class BindingIndirector
{
    public static string GetBindingSource(DependencyObject dob)
    {
        return (string)dob.GetValue(BindingSourceProperty);
    }

    public static void SetBindingSource(DependencyObject dob, string value)
    {
        dob.SetValue(BindingSourceProperty, value);
    }

    /// <summary>
    /// The "source" to be set on the binding.
    /// Must be specified.
    /// </summary>
    public static readonly DependencyProperty BindingSourceProperty =
        DependencyProperty.RegisterAttached(
            "BindingSource",
            typeof(String),
            typeof(BindingIndirector),
            new PropertyMetadata(null, BindingChanged));


    public static object GetBindingSourceContext(DependencyObject dob)
    {
        return dob.GetValue(BindingSourceContextProperty);
    }

    public static void SetBindingSourceContext(DependencyObject dob, object value)
    {
        dob.SetValue(BindingSourceContextProperty, value);
    }

    /// <summary>
    /// A DataContext type property. This overrides the inherited DataContext that would otherwise be
    /// used for the binding.
    /// Optional.
    /// </summary>
    public static readonly DependencyProperty BindingSourceContextProperty =
        DependencyProperty.RegisterAttached(
            "BindingSourceContext",
            typeof(object),
            typeof(BindingIndirector),
            new PropertyMetadata(null, BindingChanged));


    public static string GetBindingTarget(DependencyObject dob)
    {
        return (string)dob.GetValue(BindingTargetProperty);
    }

    public static void SetBindingTarget(DependencyObject dob, string value)
    {
        dob.SetValue(BindingTargetProperty, value);
    }

    /// <summary>
    /// The binding target property.
    /// Optional (defaults to "Content" if not specified
    /// </summary>
    public static readonly DependencyProperty BindingTargetProperty =
        DependencyProperty.RegisterAttached(
            "BindingTarget",
            typeof(String),
            typeof(BindingIndirector),
            new PropertyMetadata("Content", BindingChanged));

    private static void BindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(e.Property == BindingSourceContextProperty || e.NewValue is string))
            throw new ArgumentException("Property can only be set to string values", e.Property.ToString());

        // Check rules for attempting to set the binding are met
        string source = GetBindingSource(d) as string;
        string target = GetBindingTarget(d) as string;
        object context = GetBindingSourceContext(d);
        if (source == null)     // Source needs to be set - don't interfere with binding if it isn't
            return;

        // Clear any existing binding
        var originalName = e.Property ==
            BindingSourceProperty ?
                target :
                e.OldValue as string;

        if (originalName != null)
        {
            var existingDescriptor =
                DependencyPropertyDescriptor.FromName(
                    originalName,
                    d.GetType(),
                    d.GetType());

            if (existingDescriptor != null)
                d.ClearValue(existingDescriptor.DependencyProperty);
        }

        // Create and assign new binding
        var targetDescriptor = 
                DependencyPropertyDescriptor.FromName(
                    target,
                    d.GetType(),
                    d.GetType());

        if (targetDescriptor != null)   // don't interfere with binding if target invalid
        {
            Binding newBinding = new Binding(source) { Mode = BindingMode.TwoWay };
            if (context != null)        // Will fall back to DataContext of element in this case
                newBinding.Source = context;

            BindingOperations.SetBinding(d, targetDescriptor.DependencyProperty, newBinding);
        }
    }
}

This static class creates 3 attached properties and also contains a single method, "BindingChanged()" which is the propertyChangedCallback for all three APs. If enough information has been given to attempt to create a binding it does so, discarding any previous binding the APs have been used to create first.

What it doesn't do (which might be a solution) is find out whether the binding would succeed first or catch any errors produced by the binding engine (can you do that?). There might be a challenge in not suppressing binding errors that should show (because the end user has supplied duff info, for example).

Here is an example of one use-case:

<UserControl x:Class="UtilityControls.ListEditor"
             ...>

    <Grid x:Name="ControlContainer">
        <Grid.DataContext>
            <local:LeViewModel x:Name="vm" />
        </Grid.DataContext>

        <ListBox
        x:Name="EditingArea"
        ItemsSource="{Binding ColumnCollection, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:ListEditor}}}"
        >
            <ListBox.Resources>

                <DataTemplate x:Key="TextTemplate">
                    <StackPanel>
                        <TextBlock Text="{Binding DisplayName}" />
                        <TextBox 
                            local:BindingIndirector.BindingSourceContext="{Binding DataContext.CurrentEditing, ElementName=ControlContainer}"
                            local:BindingIndirector.BindingSource="{Binding PathName}"
                            local:BindingIndirector.BindingTarget="Text"
                            />
                    </StackPanel>
                </DataTemplate>

                <DataTemplate x:Key="PickListTemplate" .. />
                <DataTemplate x:Key="BooleanTemplate" ... />

            </ListBox.Resources>

            <ListBox.ItemTemplateSelector>
                <local:DataTypeSelector
                    TextTemplate="{StaticResource TextTemplate}"
                    PickListTemplate="{StaticResource PickListTemplate}"
                    BooleanTemplate="{StaticResource BooleanTemplate}"
                    />
            </ListBox.ItemTemplateSelector>

        </ListBox>
    </Grid>
</UserControl>

"CurrentEditing" is the ViewModel object the various ListBox items are editing (each ListBox item from ColumnCollection generates an editor for a different property of the object).

Hopefully the purpose of the APs (used here in "TextTemplate") is self-explanatory (they create a binding for the Text property of the TextBox), but note that although all three are necessary here, I want at the least BindingSourceContext to be optional... and this creates the problem: BindingChanged() doesn't know how many of the APs will be set, so it doesn't know when to create the binding. As a result, it has a go each time a property is changed, if it has enough information to do so. If there's more to come, then binding errors are produced.

Upvotes: 0

Views: 97

Answers (1)

Tim Oehler
Tim Oehler

Reputation: 109

You can use FallbackValue on the Binding to suppress these exceptions. For example:

<Grid Visibility="{Binding SomeProperty, FallbackValue=Collapsed}"/>

Upvotes: 2

Related Questions