Skip to content

Commit

Permalink
Merge pull request #12725 from unoplatform/dev/xygu/20230614/robertl-…
Browse files Browse the repository at this point in the history
…vs-converter-debugging

Converters on VisualStates won't be triggered more than once
  • Loading branch information
jeromelaban authored Jul 19, 2023
2 parents 4f4ba17 + d9d65ae commit 54041db
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<UserControl x:Class="Uno.UI.RuntimeTests.When_Refresh_Setter_BindingOnInvocation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="using:Uno.UI.RuntimeTests"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<UserControl.Resources>
<local:When_Refresh_Setter_BindingOnInvocation_Converter x:Key="testConverter" />
</UserControl.Resources>

<ContentControl x:Name="root"
x:FieldModifier="public">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="ContentElement_CompositeTransform.TranslateY"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource testConverter}, TargetNullValue=-1, FallbackValue=-2}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="ContentElement_CompositeTransform.TranslateY"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource testConverter}, TargetNullValue=-3, FallbackValue=-4}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>

<Border x:Name="ContentElement">
<ContentPresenter />
<Border.RenderTransform>
<CompositeTransform x:Name="ContentElement_CompositeTransform" />
</Border.RenderTransform>
</Border>
</Border>
</ControlTemplate>

</ContentControl.Template>
</ContentControl>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
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 User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236

namespace Uno.UI.RuntimeTests
{
public sealed partial class When_Refresh_Setter_BindingOnInvocation : UserControl
{
public When_Refresh_Setter_BindingOnInvocation()
{
InitializeComponent();
}
}

public class When_Refresh_Setter_BindingOnInvocation_Converter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var ctrl = value as ContentControl;

if (ctrl is not null)
{
return ctrl.Tag ?? -10;
}

return -10;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<UserControl x:Class="Uno.UI.RuntimeTests.When_Refresh_Setter_BindingOnInvocation_ElementName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="using:Uno.UI.RuntimeTests"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<UserControl.Resources>
<local:When_Refresh_Setter_BindingOnInvocation_ElementName_Converter x:Key="testConverter" />
</UserControl.Resources>

<StackPanel x:Name="sp01"
x:FieldModifier="public">
<ContentControl x:Name="root"
x:FieldModifier="public">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="ContentElement_CompositeTransform.TranslateY"
Value="{Binding ElementName=sp01, Converter={StaticResource testConverter}, TargetNullValue=-1, FallbackValue=-2}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="ContentElement_CompositeTransform.TranslateY"
Value="{Binding ElementName=sp01, Converter={StaticResource testConverter}, TargetNullValue=-3, FallbackValue=-4}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>

<Border x:Name="ContentElement">
<ContentPresenter />
<Border.RenderTransform>
<CompositeTransform x:Name="ContentElement_CompositeTransform" />
</Border.RenderTransform>
</Border>
</Border>
</ControlTemplate>

</ContentControl.Template>
</ContentControl>
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
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 User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236

namespace Uno.UI.RuntimeTests
{
public sealed partial class When_Refresh_Setter_BindingOnInvocation_ElementName : UserControl
{
public When_Refresh_Setter_BindingOnInvocation_ElementName()
{
InitializeComponent();
}
}

public class When_Refresh_Setter_BindingOnInvocation_ElementName_Converter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var ctrl = value as FrameworkElement;

if (ctrl is not null)
{
return ctrl.Tag ?? -10;
}

return -10;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
}
117 changes: 91 additions & 26 deletions src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using Windows.UI.Xaml.Markup;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Private.Infrastructure;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml
{
Expand All @@ -27,32 +29,95 @@ public async Task When_SetChildTemplateUsingVisualState()
Assert.AreEqual("Template loaded!", tb.Text);
}

private const string _when_SetChildTemplateUsingVisualState = @"
<Button xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button.Template>
<ControlTemplate TargetType=""ContentControl"">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name=""CommonStates"">
<VisualState x:Name=""Normal"">
<VisualState.Setters>
<Setter Target=""_sut.Template"">
<Setter.Value>
<ControlTemplate TargetType=""ContentControl"">
<TextBlock Text=""Template loaded!"" />
</ControlTemplate>
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentControl x:Name=""_sut"" />
</Grid>
</ControlTemplate>
</Button.Template>
</Button>";
private const string _when_SetChildTemplateUsingVisualState =
"""
<Button xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button.Template>
<ControlTemplate TargetType="ContentControl">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="_sut.Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<TextBlock Text="Template loaded!" />
</ControlTemplate>
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentControl x:Name="_sut" />
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
""";

[TestMethod]
[RunsOnUIThread]
public async Task When_Refresh_Setter_BindingOnInvocation()
{
var SUT = new When_Refresh_Setter_BindingOnInvocation();
TestServices.WindowHelper.WindowContent = SUT;
await TestServices.WindowHelper.WaitForIdle();

SUT.root.Content = 42;

var testTransform = (SUT.root.FindName("ContentElement") as FrameworkElement).RenderTransform as CompositeTransform;

Assert.IsNotNull(testTransform);

Assert.AreEqual(0, testTransform.TranslateX);

VisualStateManager.GoToState(SUT.root, "Normal", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(-10, testTransform.TranslateY);

SUT.root.Tag = 42;
VisualStateManager.GoToState(SUT.root, "Focused", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(42, testTransform.TranslateY);

SUT.root.Tag = 43;
VisualStateManager.GoToState(SUT.root, "Normal", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(43, testTransform.TranslateY);
}

[TestMethod]
[RunsOnUIThread]
public async Task When_Refresh_Setter_BindingOnInvocation_ElementName()
{
var SUT = new When_Refresh_Setter_BindingOnInvocation_ElementName();
TestServices.WindowHelper.WindowContent = SUT;
await TestServices.WindowHelper.WaitForIdle();

SUT.root.Content = 42;

var testTransform = (SUT.root.FindName("ContentElement") as FrameworkElement).RenderTransform as CompositeTransform;

Assert.IsNotNull(testTransform);

Assert.AreEqual(0, testTransform.TranslateX);

VisualStateManager.GoToState(SUT.root, "Normal", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(-10, testTransform.TranslateY);

SUT.sp01.Tag = 42;
VisualStateManager.GoToState(SUT.root, "Focused", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(42, testTransform.TranslateY);

SUT.sp01.Tag = 43;
VisualStateManager.GoToState(SUT.root, "Normal", false);
await TestServices.WindowHelper.WaitForIdle();
Assert.AreEqual(43, testTransform.TranslateY);
}
}
}
22 changes: 22 additions & 0 deletions src/Uno.UI/DataBinding/BindingExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,28 @@ internal void ResumeBinding()
}
}

/// <summary>
/// Refreshes the value to the target, as the bound source may not be observable
/// </summary>
internal void RefreshTarget()
{
ApplyElementName();

if (
// If a listener is set, ApplyBindings has been invoked
_bindingPath.ValueChangedListener is not null

// If this is not an x:Bind
&& _updateSources is null

// If there's a valid DataContext
&& GetWeakDataContext() is { IsAlive: true } weakDataContext)
{
// Apply the source on the target again (e.g. to reevaluate converters)
_bindingPath.SetWeakDataContext(weakDataContext);
}
}

/// <summary>
/// Turns UpdateSourceTrigger.Default to DependencyProperty's FrameworkPropertyMetadata.DefaultUpdateSourceTrigger
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Uno.UI/UI/Xaml/Setter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ public Setter()
private DependencyProperty? _property;
private TargetPropertyPath? _target;

// This property is not part of the WinUI API, but is
// required to determine if the value has a binding set
private static DependencyProperty InternalValueProperty { get; }
= DependencyProperty.Register(
nameof(Value),
typeof(object),
typeof(Setter),
new FrameworkPropertyMetadata(default(object)));

public object? Value
{
get
Expand Down Expand Up @@ -145,6 +154,8 @@ internal void ApplyValue(DependencyPropertyValuePrecedences precedence, IFramewo

if (path != null)
{
RefreshBindingPath();

if (ThemeResourceKey.HasValue && ResourceResolver.ApplyVisualStateSetter(ThemeResourceKey.Value, ThemeResourceContext, path, precedence, ResourceBindingUpdateReason))
{
// Applied as theme binding, no need to do more
Expand All @@ -157,6 +168,10 @@ internal void ApplyValue(DependencyPropertyValuePrecedences precedence, IFramewo
}
}

private void RefreshBindingPath()
// force binding value to re-evaluate the source and use converters
=> GetBindingExpression(InternalValueProperty)?.RefreshTarget();

private BindingPath? TryGetOrCreateBindingPath(DependencyPropertyValuePrecedences precedence, IFrameworkElement owner)
{
if (_bindingPath != null)
Expand Down
Loading

0 comments on commit 54041db

Please sign in to comment.