Multiple bindings to show dynamic content

2019-08-22 12:16发布

问题:

When creating layout with dynamic content I often do something like:

<Grid Visibility="{Binding IsLastSelectedItem, Converter=...}" >
    <Grid Visibility="{Binding IsStatisticAvailable, Converter=...}" >
        <TextBlock Visibility="{Binding HasStatistic, Converter=...}"
                   Text="{Binding Statistic}" />
    </Grid>
</Grid>

Here 2 containers are is used only to show something based on multiple conditions, it's 3 bindings combined with logical AND.

Using MVVM it is possible to create single property and bind to it directly:

public bool ShowStatistic => IsLastSelectedItem && IsStatisticAvailable && HasStatistic;

But it's not always possible/easy and has downsides. I have to monitor for changes of all conditional properties and rise notification for resulting property. If one of conditional properties is static or view-specific, then it's unavoidable hassle of adding event handlers, subscribing/unsubscribing, etc. to make it available in viewmodel and/or rise notification.

Yesterday with SO help I've created nice control to add dynamic content. It has a single bool dependency property to show/hide its content. Now I am thinking how to avoid nesting multiple of such controls for multiple bindings as in example above.

Question: what would be the best (reusable, easy to use, short, clear to understand) way to manage multiple binding used to create layout with dynamic content? I am probably lacking proper words to find similar questions.


I could think of multibinding and converter. Reusable? Hell no. Or not?

I could think of creating custom container (MyGrid) with multiple bool properties, used by multiple bindings and some other properties to specify expression: AND, OR, etc.

Maybe I am missing something obvious and easy?

回答1:

In this instance, a Multi-Value Converter is ideal.

Something like the following:

public class MultiBoolToVisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if(values.All(v=>v is bool))
            return values.All(v=>(bool)v)?
                Visibility.Visible:
                Visibility.Hidden;
        else
            throw new ArgumentException("Cannot determine boolean state of non-boolean value");
    }
}

This way you've got an expandable converter that takes one or more boolean values and returns 'Visible' only when all items in the 'values' array are true.

In your xaml:

<TextBlock Text="{Binding Statistic}" >
    <TextBlock.Visibility>
        <MultiBinding Converter="{StaticResource MultiBoolToVisibilityConverter }">
            <Binding Path="IsLastSelectedItem" />
            <Binding Path="IsStatisticAvailable" />
            <Binding Path="HasStatistic" />
        </MultiBinding>
    </TextBlock.Visibility>
</TextBlock>

Highly re-usable in any area where you have multiple flags to determine visibility, plus it's unit-testable too.



回答2:

Here is a solution using attached properties:

public static class Logic
{
    public enum Equation { Empty, AandBorCandD, ... }; // more options

    public static bool GetA(DependencyObject obj) => (bool)obj.GetValue(AProperty);
    public static void SetA(DependencyObject obj, bool value) => obj.SetValue(AProperty, value);
    public static readonly DependencyProperty AProperty =
        DependencyProperty.RegisterAttached("A", typeof(bool), typeof(Logic), new PropertyMetadata(OnValueChanged));

    // reduced content, normal attached properties, defined similar to AProperty above
    public static bool GetB... // BProperty
    public static bool GetC... // CProperty
    public static bool GetD... // DProperty
    public static Equation GetEquation... // EquationProperty
    public static bool GetR... // RProperty = result

    static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        switch (GetEquation(obj))
        {
            case Equation.AandBorCandD:
                SetR(obj, GetA(obj) && GetB(obj) || GetC(obj) && GetD(obj));
                break;
            ... // other options
        }
    }

The idea is to use several attached properties for bindings and bind needed property to "result", which is recalculated every time something is changed (similar to multi-binding).

Equation is specified as enum and there is a switch/case to calculate result.

The usage is easy:

<TextBlock local:Logic.A="{Binding ...}"
           local:Logic.B="{Binding ...}"
           local:Logic.C="{Binding ...}"
           local:Logic.D="{Binding ...}"
           local:Logic.Equation="AandBorCandD"
           Visibility="{Binding (local:Logic.R), RelativeSource={RelativeSource Self}, Converter=...}" />

Notes:

  • Attached property as binding source require that () around path.
  • This solution can be used only for a single binding per dependency object.