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?
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.
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.