Closed VincentH-Net closed 2 years ago
For WinUI this proposal exists to add support for DataTemplate
, ItemsPanelTemplate
and ControlTemplate
.
C# Markup already contains a workaround for DataTemplate
using XamlReader
Consider the same approach for other types derived from FrameworkTemplate
, including ControlTemplates
Encountered a blocker using the approach suggested above.
Can anyone explain how I can set (or cause to be set) TemplatedParent
on child elements that I add from C# to a ControlTemplate
instance that was created via XamlReader
?
I dynamically add child controls to a root Grid
which is in a ControlTemplate
via a custom attached property on that Grid. The controls show up fine where the template is applied.
However ContentPresenter
and TemplateBinding
added via C# do not work. I noticed thatTemplatedParent
is set on the Grid which was in the XAML, however it is not set not on the child controls I add to the Grid from C# and I suspect this may be the cause.
Any explanation / alternatives appreciated!
ControlTemplates are now supported in Uno Platform and WPF, but not in WinUI 3 (Windows Desktop).
XamlWriter
so we cannot use that for a workaround, and WPF also does not expose creating a template from C# like UNO does. Better workarounds may be possible; see workaround implementation for details.TemplatedParent
property read access.Have you taken a look a x:Bind for templated parents? The underlying APIs used by the WinUI code generator may help.
@jeromelaban I have not; I'd be happy to check that out. Could you point me to an example of the relevant code - for WinUI? I don't really know where to start looking.
I don't, but you can create a template with x:Bind to templated parent, and look at the generated .g.cs
files in the obj folder.
@jeromelaban wrote:
you can create a template with x:Bind to templated parent, and look at the generated
.g.cs
files in the obj folder.
The templated parent is passed into the generated code by the XAML runtime, which is what we need at minimum for a workaround in WinUI 3 Desktop. However I have not found if/how that call could be triggered from C#. Below are my findings.
@jeromelaban looking at below, can you think of any further avenues on how to specify connectionId
s from C#? Or alternatively, do you know whether my theory on how connectionId
s are supplied when using XAML (see below) is correct?
I investigated the code that WinUI 3 x:Bind
generates for below example, which binds the Text
property of a TextBlock
to the Content
property of the templated parent (a Button
):
<Button Content="Click Me!" >
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid>
<TextBlock Text="{x:Bind Content, Mode=OneWay }"/>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
The generated page code adds an implementation of IComponentConnector to the page. Here is the relevant part of the implementation:
public IComponentConnector GetBindingConnector(int connectionId, object target)
{
// ...
switch(connectionId)
{
case 2: // BlankPage1.xaml line 14
{
var templatedParent2 = target as global::Microsoft.UI.Xaml.Controls.Button;
// ...
}
break;
}
// ...
}
The connectionId
parameter is an identifier token to distinguish calls - in this example, 2
represents the binding expression for the Text
property of the TextBlock
in above XAML. However there are no GetBindingConnector
invocations in any generated C# and the number 2
is not passed in from generated C# anywhere.
The problem is that GetBindingConnector
is invoked by the XAML runtime, and that the values for connectionId
that are passed into this method by the runtime are not specified in (generated) C# (unless reflection is used by the runtime to derive the number from C# generated code names, but that seems unlikely since the main point of x:Bind
is to add a binding mechanism that does not require reflection).
The connectionId
for each binding expression in the XAML is either (re)calculated at runtime from the XAML resource by the XAML runtime, or - more likely - it is stored in another type of XAML-derived metadata resource (XBF?).
Current situation: without a way to tell the XAML runtime which connectionId
's to call for a templated parent, the API's used in the code generated by x:Bind
cannot be leveraged to create working template bindings in control templates from C#.
An alternative workaround (not using the API's that x:Bind
uses or even an actual ControlTemplate
) would be to provide guidance on how to use standard C# language features to implement the functionality of templates.
E.g. simply define your own factory method for each templated control.
@jeromelaban looking at below, can you think of any further avenues on how to specify
connectionId
s from C#? Or alternatively, do you know whether my theory on howconnectionId
s are supplied when using XAML (see below) is correct?
I do not have this kind of details, figuring it out is likely to be a guessing game of wack-a-mole :)
One avenue that I could explore, given all these blockers, is to provide an alternative C# implementation for the C# Markup control template helpers for WinUI desktop (ControlTemplate
and BindTemplate
methods), that does not use the XAML ControlTemplates
at all.
This would allow to use the same C# Markup for WinUI desktop and Uno; the WinUI implementation would work like a factory method - for each templated parent a new set of the same controls and bindings would be built in C#. The performance would be similar to the workaround used in WPF.
Would something like this help?
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
var template = XamlReader.Load(@"
<ControlTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:helper=""using:Net6WinUI01"" TargetType=""ContentControl"">
<Grid>
<helper:TemplateBindingHelper TemplatedParent=""{Binding RelativeSource={RelativeSource TemplatedParent}}"" Content=""{TemplateBinding Content}"" />
</Grid>
</ControlTemplate>
") as ControlTemplate;
var content = new ContentControl() {
Template = template,
Content = new Border() // user written visual code
};
content.Loaded += delegate
{
var templatedParent = TemplateBindingHelper.FindTemplatedParent(content.ContentTemplateRoot);
// Here, template == content
};
Content = content;
}
}
[Bindable]
public class TemplateBindingHelper : ContentPresenter
{
public TemplateBindingHelper() { }
public UIElement TemplatedParent
{
get => (UIElement)GetValue(TemplatedParentProperty);
set => SetValue(TemplatedParentProperty, value);
}
public static readonly DependencyProperty TemplatedParentProperty =
DependencyProperty.Register("TemplatedParent", typeof(UIElement), typeof(TemplateBindingHelper), new PropertyMetadata(null));
internal static UIElement FindTemplatedParent(DependencyObject element)
{
while(VisualTreeHelper.GetParent(element) is { } parent)
{
if(parent is TemplateBindingHelper helper)
{
return helper.TemplatedParent;
}
element = parent;
}
return null;
}
}
@jeromelaban wrote:
Would something like this help?
That works as-is to access the templated parent - thanks!
I tried to simplify that by making the TemplatedParentProperty
an attached property.
However when I load below Xaml, and apply the resulting template to a control, the template renders OK and a breakpoint in BuildChild.SetId
is hit but in BuildChild.SetTemplatedParent
it is not hit.
-> Any idea why this binding expression would not work as an attached property in a control template?
If I change the type of TemplatedParentProperty
to string
and set the value to a string in the XAML the breakpoint is hit.
I would expect it to work as in your example the same binding is used in the XAML.
<ControlTemplate TargetType="Button"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x= "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:root="using:Microsoft.UI.Xaml.Controls"
xmlns:delegators="using:CSharpMarkup.WinUI.Delegators">
<root:Grid delegators:BuildChild.TemplatedParent="{Binding RelativeSource={RelativeSource TemplatedParent}}" delegators:BuildChild.Id="WinUICsMarkupExamples.FlutterPage+<>c.<.ctor>b__2_0" />
</ControlTemplate>
This is the BuildChild
implementation:
[Bindable]
public class BuildChild
{
static Dictionary<string, Func<CSharpMarkup.WinUI.UIElement>> delegates = null;
static string Id(Delegate build) => $"{build.Method.DeclaringType.FullName}.{build.Method.Name}";
[Conditional("DEBUG")]
internal static void AssertStateless(Delegate build)
{
if (build.Target != null)
{
string fields = string.Join(',', build.Method.DeclaringType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly).Select(f => f.Name));
string properties = string.Join(',', build.Method.DeclaringType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly).Select(p => p.Name));
string state = string.Join(',', fields, properties);
if (state != ",")
throw new ArgumentException($"State in template methods is not supported: {state}. Either avoid state in the lambda expression or use a static method, e.g. DataTemplate(AStaticMethod)", nameof(build));
}
}
public static string CreateIdFor(Func<CSharpMarkup.WinUI.UIElement> build)
{
AssertStateless(build);
if (delegates == null) delegates = new Dictionary<string, Func<CSharpMarkup.WinUI.UIElement>>();
string id = Id(build);
delegates[id] = build;
return id;
}
public static Xaml.DependencyProperty IdProperty = Xaml.DependencyProperty.RegisterAttached("Id", typeof(string), typeof(BuildChild), new Xaml.PropertyMetadata(null));
public static string GetId(Microsoft.UI.Xaml.Controls.Panel panel) => (string)panel.GetValue(IdProperty);
public static void SetId(Microsoft.UI.Xaml.Controls.Panel panel, string id)
{
panel.SetValue(IdProperty, id);
panel.Children.Clear();
if (!string.IsNullOrEmpty(id)) panel.Children.Add(delegates[id]().UI);
}
public static Xaml.DependencyProperty TemplatedParentProperty = Xaml.DependencyProperty.RegisterAttached("TemplatedParent", typeof(Microsoft.UI.Xaml.UIElement), typeof(BuildChild), new Xaml.PropertyMetadata(null));
public static Xaml.UIElement GetTemplatedParent(Microsoft.UI.Xaml.Controls.Panel panel)
=> (Xaml.UIElement)panel.GetValue(TemplatedParentProperty);
public static void SetTemplatedParent(Microsoft.UI.Xaml.Controls.Panel panel, Microsoft.UI.Xaml.UIElement parent)
=> panel.SetValue(TemplatedParentProperty, parent);
}
The property is changed, but not through the SetTemplatedParent
method, but rather the DP system underneath.
If you do this, you'll get the new value:
public static DependencyProperty TemplatedParentProperty = DependencyProperty.RegisterAttached(
"TemplatedParent", typeof(Microsoft.UI.Xaml.UIElement), typeof(BuildChild), new PropertyMetadata(null, (s, e) => { }));
In general, it's best not to rely on the methods being invoked, as the value can be changed by other means (animations being one).
@jeromelaban wrote:
In general, it's best not to rely on the methods being invoked, as the value can be changed by other means (animations being one).
This works, thanks! I will integrate it for the next release.
Fixed in release 0.8.2
As discussed: