Skip to content

Commit

Permalink
feat: Add x:Bind support for events
Browse files Browse the repository at this point in the history
Fixes #3172
  • Loading branch information
jeromelaban committed May 29, 2020
1 parent 72c6603 commit 2b81c0e
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 94 deletions.
14 changes: 12 additions & 2 deletions doc/articles/features/windows-ui-xaml-xbind.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Uno Support for x:Bind

Uno supports the [`x:Bind`](https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension) WinUI feature, which gives the ability the bind to normal fields and properties, static classes fields, and functions with multiple parameters.
Uno supports the [`x:Bind`](https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension) WinUI feature, which gives the ability the bind to normal fields and properties, static classes fields, functions with multiple parameters, and events.

# Examples
- Properties
Expand Down Expand Up @@ -67,5 +67,15 @@ Uno supports the [`x:Bind`](https://docs.microsoft.com/en-us/windows/uwp/xaml-pl
}
```

- Bind to events
```xaml
<CheckBox Checked="{x:Bind OnCheckedRaised}" Unchecked="{x:Bind OnUncheckedRaised}" />
```
where these methods are available in the code behind:
```csharp
public void OnCheckedRaised() { }
public void OnUncheckedRaised(object sender, RoutedEventArgs args) { }
```

# Not supported
- Type casts
- Type casts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static class Types
public const string DependencyObjectExtensions = BaseXamlNamespace + ".DependencyObjectExtensions";
public const string DependencyProperty = BaseXamlNamespace + ".DependencyProperty";
public const string IFrameworkElement = UnoXamlNamespace + ".IFrameworkElement";
public const string FrameworkElement = UnoXamlNamespace + ".FrameworkElement";
public const string UIElement = UnoXamlNamespace + ".UIElement";
public const string Style = BaseXamlNamespace + ".Style";
public const string ElementStub = BaseXamlNamespace + ".ElementStub";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,15 +626,16 @@ private void BuildChildSubclasses(IIndentedStringBuilder writer, bool isTopLevel
using (writer.BlockInvariant("public {0} Build()", kvp.Value.ReturnType))
{
writer.AppendLineInvariant("var nameScope = new global::Windows.UI.Xaml.NameScope();");
writer.AppendLineInvariant("var child = ");
writer.AppendLineInvariant($"{kvp.Value.ReturnType} __rootInstance = null;");
writer.AppendLineInvariant("__rootInstance = ");

// Is never considered in Global Resources because class encapsulation
BuildChild(writer, contentOwner, contentOwner.Objects.First());

writer.AppendLineInvariant(";");
writer.AppendLineInvariant("if (child is DependencyObject d) Windows.UI.Xaml.NameScope.SetNameScope(d, nameScope);");
writer.AppendLineInvariant("if (__rootInstance is DependencyObject d) Windows.UI.Xaml.NameScope.SetNameScope(d, nameScope);");

writer.AppendLineInvariant("return child;");
writer.AppendLineInvariant("return __rootInstance;");
}

BuildBackingFields(writer);
Expand Down Expand Up @@ -2460,57 +2461,7 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
}
else if (eventSymbol != null)
{
// If a binding is inside a DataTemplate, the binding root in the case of an x:Bind is
// the DataContext, not the control's instance.
var isInsideDataTemplate = IsMemberInsideFrameworkTemplate(member.Owner);

void writeEvent(string ownerPrefix)
{
if (eventSymbol.Type is INamedTypeSymbol delegateSymbol)
{
var parms = delegateSymbol
.DelegateInvokeMethod
.Parameters
.Select(p => member.Value + "_" + p.Name)
.JoinBy(",");

var eventSource = ownerPrefix.HasValue() ? ownerPrefix : "this";

//
// Generate a weak delegate, so the owner is not being held onto by the delegate. We can
// use the WeakReferenceProvider to get a self reference to avoid adding the cost of the
// creation of a WeakReference.
//
writer.AppendLineInvariant($"var {member.Member.Name}_{member.Value}_That = ({eventSource} as global::Uno.UI.DataBinding.IWeakReferenceProvider).WeakReference;");
writer.AppendLineInvariant($"{closureName}.{member.Member.Name} += ({parms}) => ({member.Member.Name}_{member.Value}_That.Target as {_className.className})?.{member.Value}({parms});");
}
else
{
GenerateError(writer, $"{eventSymbol.Type} is not a supported event");
}
}

if (!isInsideDataTemplate)
{
writeEvent("");
}
else if (_className.className != null)
{
_hasLiteralEventsRegistration = true;
writer.AppendLineInvariant($"{closureName}.RegisterPropertyChangedCallback(");
using (writer.BlockInvariant($"global::Uno.UI.Xaml.XamlInfo.XamlInfoProperty, (s, p) =>"))
{
using (writer.BlockInvariant($"if (global::Uno.UI.Xaml.XamlInfo.GetXamlInfo({closureName})?.Owner is {_className.className} owner)"))
{
writeEvent("owner");
}
}
writer.AppendLineInvariant($");");
}
else
{
GenerateError(writer, $"Unable to use event {member.Member.Name} without a backing class (use x:Class)");
}
GenerateInlineEvent(closureName, writer, member, eventSymbol);
}
else
{
Expand Down Expand Up @@ -2565,6 +2516,111 @@ void BuildCustomMarkupExtensionPropertyValue(IIndentedStringBuilder writer, Xaml
}
}

private void GenerateInlineEvent(string closureName, IIndentedStringBuilder writer, XamlMemberDefinition member, IEventSymbol eventSymbol)
{
// If a binding is inside a DataTemplate, the binding root in the case of an x:Bind is
// the DataContext, not the control's instance.
var isInsideDataTemplate = IsMemberInsideDataTemplate(member.Owner);

void writeEvent(string ownerPrefix)
{
if (eventSymbol.Type is INamedTypeSymbol delegateSymbol)
{
var parms = delegateSymbol
.DelegateInvokeMethod
.Parameters
.Select(p => member.Value + "_" + p.Name)
.JoinBy(",");

var eventSource = ownerPrefix.HasValue() ? ownerPrefix : "this";

if (member.Objects.FirstOrDefault() is XamlObjectDefinition bind && bind.Type.Name == "Bind")
{
var eventTarget = XBindExpressionParser.RestoreSinglePath(bind.Members.First().Value?.ToString());

(string target, string weakReference, INamedTypeSymbol sourceType) buildTargetContext()
{
if (isInsideDataTemplate.isInside)
{
var dataTypeObject = FindMember(isInsideDataTemplate.xamlObject, "DataType", XamlConstants.XamlXmlNamespace)
?? throw new Exception($"Unable to find x:DataType in enclosing DataTemplate for x:Bind event");
var dataTypeSymbol = GetType(dataTypeObject.Value?.ToString());

return (
$"({member.Member.Name}_{eventTarget}_That.Target as {XamlConstants.Types.FrameworkElement})?.DataContext as {dataTypeSymbol}",

// Use of __rootInstance is required to get the top-level DataContext, as it may be changed
// in the current visual tree by the user.
$"(__rootInstance as global::Uno.UI.DataBinding.IWeakReferenceProvider).WeakReference",
dataTypeSymbol
);
}
else
{
return (
$"{member.Member.Name}_{eventTarget}_That.Target as {_className.className}",
$"({eventSource} as global::Uno.UI.DataBinding.IWeakReferenceProvider).WeakReference",
FindType(_className.className)
);
}
}

var targetContext = buildTargetContext();

var targetMethodHasParamters = targetContext.sourceType.GetMethods().FirstOrDefault(m => m.Name == eventTarget)?.Parameters.Any() ?? false;
var xBindParams = targetMethodHasParamters ? parms : "";

//
// Generate a weak delegate, so the owner is not being held onto by the delegate. We can
// use the WeakReferenceProvider to get a self reference to avoid adding the cost of the
// creation of a WeakReference.
//
writer.AppendLineInvariant($"var {member.Member.Name}_{eventTarget}_That = {targetContext.weakReference};");

writer.AppendLineInvariant($"{closureName}.{member.Member.Name} += ({parms}) => ({targetContext.target})?.{eventTarget}({xBindParams});");
}
else
{

//
// Generate a weak delegate, so the owner is not being held onto by the delegate. We can
// use the WeakReferenceProvider to get a self reference to avoid adding the cost of the
// creation of a WeakReference.
//
writer.AppendLineInvariant($"var {member.Member.Name}_{member.Value}_That = ({eventSource} as global::Uno.UI.DataBinding.IWeakReferenceProvider).WeakReference;");

writer.AppendLineInvariant($"{closureName}.{member.Member.Name} += ({parms}) => ({member.Member.Name}_{member.Value}_That.Target as {_className.className})?.{member.Value}({parms});");
}
}
else
{
GenerateError(writer, $"{eventSymbol.Type} is not a supported event");
}
}

if (!isInsideDataTemplate.isInside)
{
writeEvent("");
}
else if (_className.className != null)
{
_hasLiteralEventsRegistration = true;
writer.AppendLineInvariant($"{closureName}.RegisterPropertyChangedCallback(");
using (writer.BlockInvariant($"global::Uno.UI.Xaml.XamlInfo.XamlInfoProperty, (s, p) =>"))
{
using (writer.BlockInvariant($"if (global::Uno.UI.Xaml.XamlInfo.GetXamlInfo({closureName})?.Owner is {_className.className} owner)"))
{
writeEvent("owner");
}
}
writer.AppendLineInvariant($");");
}
else
{
GenerateError(writer, $"Unable to use event {member.Member.Name} without a backing class (use x:Class)");
}
}

/// <summary>
/// Build localized properties which have not been set in the xaml.
/// </summary>
Expand Down Expand Up @@ -2750,60 +2806,67 @@ string GetBindingOptions()
return null;
}

var bindingOptions = GetBindingOptions();

if (bindingOptions != null)
if (FindEventType(member.Member) is IEventSymbol eventSymbol)
{
var isAttachedProperty = IsDependencyProperty(member.Member);
var isBindingType = FindPropertyType(member.Member) == _dataBindingSymbol;

var bindEvalFunction = bindNode != null ? BuildXBindEvalFunction(member, bindNode) : "";
GenerateInlineEvent(closureName, writer, member, eventSymbol);
}
else
{
var bindingOptions = GetBindingOptions();

if (isAttachedProperty)
if (bindingOptions != null)
{
var propertyOwner = GetType(member.Member.DeclaringType);
var isAttachedProperty = IsDependencyProperty(member.Member);
var isBindingType = FindPropertyType(member.Member) == _dataBindingSymbol;

writer.AppendLine(formatLine($"SetBinding({GetGlobalizedTypeName(propertyOwner.ToDisplayString())}.{member.Member.Name}Property, new {XamlConstants.Types.Binding}{{ {bindingOptions} }}{bindEvalFunction})"));
}
else if (isBindingType)
{
writer.AppendLine(formatLine($"{member.Member.Name} = new {XamlConstants.Types.Binding}{{ {bindingOptions} }}"));
}
else
{
writer.AppendLine(formatLine($"SetBinding(\"{member.Member.Name}\", new {XamlConstants.Types.Binding}{{ {bindingOptions} }}{bindEvalFunction})"));
}
}
var bindEvalFunction = bindNode != null ? BuildXBindEvalFunction(member, bindNode) : "";

var resourceName = GetStaticResourceName(member);
if (isAttachedProperty)
{
var propertyOwner = GetType(member.Member.DeclaringType);

if (resourceName != null)
{
var isAttachedProperty = IsAttachedProperty(member);
writer.AppendLine(formatLine($"SetBinding({GetGlobalizedTypeName(propertyOwner.ToDisplayString())}.{member.Member.Name}Property, new {XamlConstants.Types.Binding}{{ {bindingOptions} }}{bindEvalFunction})"));
}
else if (isBindingType)
{
writer.AppendLine(formatLine($"{member.Member.Name} = new {XamlConstants.Types.Binding}{{ {bindingOptions} }}"));
}
else
{
writer.AppendLine(formatLine($"SetBinding(\"{member.Member.Name}\", new {XamlConstants.Types.Binding}{{ {bindingOptions} }}{bindEvalFunction})"));
}
}

if (isAttachedProperty)
{
var propertyOwner = GetType(member.Member.DeclaringType);
var resourceName = GetStaticResourceName(member);

writer.AppendFormatInvariant("{0}.Set{1}({2},{3});\r\n",
GetGlobalizedTypeName(propertyOwner.ToDisplayString()),
member.Member.Name,
closureName,
resourceName
);
}
else
if (resourceName != null)
{
if (generateAssignation)
var isAttachedProperty = IsAttachedProperty(member);

if (isAttachedProperty)
{
writer.AppendLineInvariant(0, formatLine("{0} = {1}"),
var propertyOwner = GetType(member.Member.DeclaringType);

writer.AppendFormatInvariant("{0}.Set{1}({2},{3});\r\n",
GetGlobalizedTypeName(propertyOwner.ToDisplayString()),
member.Member.Name,
closureName,
resourceName
);
}
else
{
writer.AppendLineInvariant(resourceName);
if (generateAssignation)
{
writer.AppendLineInvariant(0, formatLine("{0} = {1}"),
member.Member.Name,
resourceName
);
}
else
{
writer.AppendLineInvariant(resourceName);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Page x:Class="Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls.Binding_Event"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid>
<CheckBox x:Name="myCheckBox"
x:FieldModifier="public"
Checked="{x:Bind OnCheckedRaised}"
Unchecked="{x:Bind OnUncheckedRaised}"/>
</Grid>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

namespace Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class Binding_Event : Page
{
public Binding_Event()
{
this.InitializeComponent();
}

public int CheckedRaised { get; private set; }
public int UncheckedRaised { get; private set; }

private void OnCheckedRaised()
{
CheckedRaised++;
}

private void OnUncheckedRaised(object sender, RoutedEventArgs args)
{
UncheckedRaised++;
}
}
}
Loading

0 comments on commit 2b81c0e

Please sign in to comment.