I am creating a settings editor where plugin writers can define their own user interface for configuring their plugins. I am implementing a feature to hide certain "advanced" elements if a checkbox is unchecked.
The checkbox XAML is trivial:
<CheckBox Name="isAdvanced">_Advanced</CheckBox>
Ideally (more on this later), implementors would just add a flag to advanced controls (which should be hidden when the "advanced" checkbox is unchecked) like so:
<Button library:MyLibraryControl.IsAdvanced="True">My Button</Button>
The problem lies in making the magic of hiding the IsAdvanced="True"
elements when isAdvanced.IsChecked == false
. I have the desired behaviour with this style on the window element:
<Window.Resources>
<Style TargetType="Button">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding (library:MyLibraryControl.IsAdvanced), RelativeSource={RelativeSource Mode=Self}}" Value="True" />
<Condition Binding="{Binding IsChecked, ElementName=isAdvanced}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="UIElement.Visibility" Value="Collapsed" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
However, this method presents two problems:
IsAdvanced
flag can (should be able to) be added to any visual element.Is there some other way to produce the functionality I want? I'm not afraid of working in the code-behind, but an elegant XAML solution is ideal (as this is purely a UI change, aside from saving the state of the checkbox in the user's preferences).
Some other methods of signifying advanced elements have come to mind. These include using a dynamic resource and directly binding:
<Button Visibility="{DynamicResource IsAdvancedVisibility}">My Button</Button>
<Button Visibility="{Binding IsChecked, RelativeSource={...}, ValueConverter={...}}">My Button</Button>
Using a resource dictionary would probably work, but it seems like a really bad solution as UI state doesn't seem like it should belong in a dictionary. Binding manually is quite the mess because the state of the checkbox has to be sent somehow to the element, and aside from hardcoding values I don't see it not becoming a mess.
Both of these alternate solutions tie semantics ("this is an advanced option") to appearance ("advanced options should be collapsed"). Coming from the HTML world, I know this is a very bad thing, and I refuse to submit to these methods unless absolutely necessary.
I decided to invert the problem a little bit, and it worked well.
Instead of dealing with styles, I used property binding as suggested by Gishu. However, instead of placing the UI in the VM (where properties would propagate several layers manually), I used an attached property named ShowAdvanced
which propagates down via property inheritance.
Creating this property is trivial:
public static readonly DependencyProperty ShowAdvancedProperty;
ShowAdvancedProperty = DependencyProperty.RegisterAttached(
"ShowAdvanced",
typeof(bool),
typeof(MyLibraryControl),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior
)
);
The checkbox sets the ShowAdvanced
property above on the entire window. It could set it elsewhere (e.g. on the grid), but putting it on the window makes more sense IMO:
<CheckBox Grid.Column="0"
IsChecked="{Binding (library:MyLibraryControl.ShowAdvanced), ElementName=settingsWindow}"
Content="_Advanced" />
Changing the visibility (or whatever other properties desired) depending on the ShowAdvanced
property becomes easy:
<Foo.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Foo.Resources>
<Button Visibility="{Binding (library:MyLibraryControl.ShowAdvanced), RelativeSource={RelativeSource Self}, Converter={StaticResource BooleanToVisibilityConverter}}">I'm Advanced</Button>
Ditching styles allows plugin writers to completely change the layout of their controls if they need to. They can also show advanced controls but keep them disabled if desired. Styles brought up a lot of problems and, as Meleak showed, the workarounds were messy.
My main problem with putting the 'advanced' display logic in the VM is that it is now less likely you can get away with binding multiple views to the same VM while maintaining the flexibility desired. If the 'advanced' logic is in the VM, advanced controls must be shown for all views or no views; you can't show them for one and hide them for another. This, IMO, breaks the principles of having a VM in the first place.
(Thanks to all who posted here; it's been helpful!)