0

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?

Sinatr
  • 20,892
  • 15
  • 90
  • 319

2 Answers2

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.

Danielle Summers
  • 366
  • 2
  • 11
  • `values.All()` is equal to `AND`. How about `OR`? I can create many such converters with different names and logic inside and use one which I need. I am not sure how to reflect which binding is what condition. E.g. if I call converter `AllTrue` or `AnyTrue` it's clear, but if I want condition `A AND B OR C AND D` (where a, b, c, d are bindings) ? Use first binding as *parameter* (string?) for converter to specify conditions in pseudo-form? Hmm.. – Sinatr Jan 30 '18 at 14:33
  • I cannot comprehend a scenario in which that could be useful, but it's possible. It would put a degree of logic into the xaml (specifying pseudo-code operands between value bindings) which makes code maintenance more difficult. For more complex solutions like what you are suggesting, it may be better to have the flag in your ViewModel. – Danielle Summers Jan 30 '18 at 14:48
  • The worst case is to have a lot of booleans in viewmodel: `ShowA`, `ShowB`, `ShowC`, etc. where A, B, C are something which is dynamically shown. That very quickly become hard to support, especially with all cross notifications (changing one property require to rise notification for many others). I was thinking to reduce complexity by moving that "show" logic into view, then viewmodel doesn't need to care about visibility, e.g. setting `HasStatistic` is all its job. I am trying to think a bit ahead, your solution is nearly perfect, maybe it lacks a bit of polishing to become reusable. – Sinatr Jan 30 '18 at 14:58
0

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.
Sinatr
  • 20,892
  • 15
  • 90
  • 319