I commonly find myself wanting to wrap one or multiple UserControl
s into a single one for reusability reasons. In other frameworks/libraries this seems to be far more common/trivial to achieve.
I take that in most frameworks utilizing Xaml, defining custom controls is not as common due to the MVVM approach, which might be the reason there is no useful documentation for this around
There are a lot of vaguely similar questions on this topic, but I failed to find one that concisely elaborates on the ideal way to achieve this.
Background
This is how I would go about in in Blazor. Defining a property with the Parameter
property results in a one-way binding and updates the component when that passed parameter changes.
[Parameter]
public bool Value { get; set; }
If Two way binding is desired, one has to define a corresponding callback explicitly and configure two-way binding inside the parent with the @bind-
syntax.
[Parameter]
public EventCallback<bool> ValueChanged { get; set; }
private async Task SetValue(bool value)
{
if (Value != value)
{
Value = value;
await ValueChanged.InvokeAsync(value);
}
}
In React this is quite similar though the parent has to explicitly pass an event callback as there is no true two-way binding.
Issue
I can't get my head around how to do this in Xaml. I take that the usual way to define a property on a custom control is a registered DependencyProperty
. My approach so far was:
- Define a
DependencyProperty
; - Bind that the property's value to the wrapped control's value;
- if it is readonly, use one-way binding;
- if it is editable from within the custom control, two-way bind to the inner control's value to the property.
Here is a sample of a slider that display's its discrete index next to it.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider x:Name="IndexSlider"
Grid.Column="0"
TickFrequency="1"
Minimum="0"
Value="{x:Bind Value, Mode=TwoWay}"
Maximum="{x:Bind Maximum, Mode=OneWay}"
Foreground="Blue"/>
<TextBlock VerticalAlignment="Center"
HorizontalAlignment="Right"
Grid.Column="1"
Margin="6,0,0,0"
Text="{x:Bind local:WrappedSlider.DiscretePaginate(IndexSlider.Value, Maximum), Mode=OneWay}"/>
</Grid>
public sealed partial class WrappedSlider : UserControl
{
public static readonly DependencyProperty ValueProperty = (/*...*/);
public double Value
{
get { return (double)GetValue(ValueProperty); }
set
{
SetValue(ValueProperty, value);
}
}
public static readonly DependencyProperty MaximumProperty = (/*...*/);
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set
{
SetValue(MaximumProperty, value);
}
}
public WrappedSlider()
{
InitializeComponent();
}
public static string DiscretePaginate(double currentIndex, double upperBound) => $"{currentIndex}/{upperBound}";
}
While this approach works for simple controls (in most cases so far) there is a plenty of things disturbing me.
My questions
- Is (one-way or two-way) binding a control's value to its wrappers passed
DependencyProperty
an issue at all or does it yield any pitfalls? It appears to me as there is no single source of truth anymore, but the inner control's value as well as theDependencyProperty
's backing field. Can someone who has no idea about my control's implementation be ensured that he can reliably two-way to my control (or one-way bind for read-only controls/when reverse binding is not needed). - Is the
x:Bind
syntax the right approach here or should I give the inner control ax:Name
and set/get its value through the outer property's getter/setter? I noticed children occasionally not updating when using{x:Bind Variable, Mode=OneWay}
and invoking the setter inside the code-behind, while doingMyControlsName.Value =
did. Or should I even use a entirely different approach. - Might
INotifyPropertyChanged
be feasible here? This post lists various advantages, but it appears to me as something that's exclusively used for theViewModel
in a MVVM architecture.
I really could use some input here. This seems like something that should be self-explanatory, but the countless ways to implement bindings in Xaml on top of the widely used MVVM approach makes this very obstructive for someone with a background in Blazor.