Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(ResExpr): prevent unnecessary updates #953

Merged
merged 2 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ public ResponsiveLayout ResponsiveLayout

#endregion

private static void OnNarrowestTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnNarrowTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnNormalTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnWideTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnWidestTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnNarrowestTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate(forceApplyValue: true);
private static void OnNarrowTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate(forceApplyValue: true);
private static void OnNormalTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate(forceApplyValue: true);
private static void OnWideTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate(forceApplyValue: true);
private static void OnWidestTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate(forceApplyValue: true);

private static void OnResponsiveLayoutChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.ResolveTemplate();
private static void OnResponsiveLayoutChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) => (sender as ResponsiveView)?.UpdateTemplate();
}
63 changes: 32 additions & 31 deletions src/Uno.Toolkit.UI/Controls/ResponsiveView/ResponsiveView.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using Windows.Foundation;
using System;

#if IS_WINUI
using Microsoft.UI.Xaml;
Expand All @@ -14,7 +13,8 @@ namespace Uno.Toolkit.UI;

public partial class ResponsiveView : ContentControl, IResponsiveCallback
{
internal ResolvedLayout<DataTemplate?>? ResolvedLayout { get; private set; }
public Layout? CurrentLayout { get; private set; }
internal (ResponsiveLayout Layout, Size Size, Layout? Result) LastResolved { get; private set; }

public ResponsiveView()
{
Expand All @@ -25,45 +25,46 @@ public ResponsiveView()
Loaded += ResponsiveView_Loaded;
}

private void ResponsiveView_Loaded(object sender, RoutedEventArgs e)
{
ResolveTemplate();
}
private void ResponsiveView_Loaded(object sender, RoutedEventArgs e) => UpdateTemplate(forceApplyValue: true);

public void OnSizeChanged(Size size, ResponsiveLayout layout)
{
ResolveTemplate(size, GetAppliedLayout() ?? layout);
}
public void OnSizeChanged(ResponsiveHelper helper) => UpdateTemplate(helper);

private void ResolveTemplate()
private void UpdateTemplate(ResponsiveHelper? helper = null, bool forceApplyValue = false)
{
if (!IsLoaded) return;

var helper = ResponsiveHelper.GetForCurrentView();
helper ??= ResponsiveHelper.GetForCurrentView();
var resolved = helper.ResolveLayout(GetAppliedLayout(), GetAvailableLayoutOptions());

ResolveTemplate(helper.WindowSize, GetAppliedLayout() ?? helper.Layout);
if (forceApplyValue || CurrentLayout != resolved.Result)
{
Content = GetTemplateFor(resolved.Result)?.LoadContent() as UIElement;

CurrentLayout = resolved.Result;
LastResolved = resolved;
}
}

private void ResolveTemplate(Size size, ResponsiveLayout layout)
private DataTemplate? GetTemplateFor(Layout? layout)
{
if (!IsLoaded) return;

var defs = new (double MinWidth, ResolvedLayout<DataTemplate?> Value)[]
return layout switch
{
(layout.Narrowest, new(nameof(layout.Narrowest), NarrowestTemplate)),
(layout.Narrow, new(nameof(layout.Narrow), NarrowTemplate)),
(layout.Normal, new(nameof(layout.Normal), NormalTemplate)),
(layout.Wide, new(nameof(layout.Wide), WideTemplate)),
(layout.Widest, new(nameof(layout.Widest), WidestTemplate)),
}.Where(x => x.Value.Value != null).ToArray();
var match = defs.FirstOrNull(y => y.MinWidth >= size.Width) ?? defs.LastOrNull();
var resolved = match?.Value;
UI.Layout.Narrowest => NarrowestTemplate,
UI.Layout.Narrow => NarrowTemplate,
UI.Layout.Normal => NormalTemplate,
UI.Layout.Wide => WideTemplate,
UI.Layout.Widest => WidestTemplate,
_ => null,
};
}

if (resolved != ResolvedLayout)
{
Content = resolved?.Value?.LoadContent() as UIElement;
ResolvedLayout = resolved;
}
private IEnumerable<Layout> GetAvailableLayoutOptions()
{
if (NarrowestTemplate != null) yield return UI.Layout.Narrowest;
if (NarrowTemplate != null) yield return UI.Layout.Narrow;
if (NormalTemplate != null) yield return UI.Layout.Normal;
if (WideTemplate != null) yield return UI.Layout.Wide;
if (WidestTemplate != null) yield return UI.Layout.Widest;
}

internal ResponsiveLayout? GetAppliedLayout() =>
Expand Down
28 changes: 24 additions & 4 deletions src/Uno.Toolkit.UI/Helpers/ResponsiveHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Uno.Toolkit.UI;

internal interface IResponsiveCallback
{
void OnSizeChanged(Size size, ResponsiveLayout layout);
void OnSizeChanged(ResponsiveHelper sender);
}

public partial class ResponsiveLayout : DependencyObject
Expand Down Expand Up @@ -113,7 +113,7 @@ public static ResponsiveLayout Create(double narrowest, double narrow, double no
public override string ToString() => "[" + string.Join(",", Narrowest, Narrow, Normal, Wide, Widest) + "]";
}

internal record ResolvedLayout<T>(string Layout, T Value);
public enum Layout { Narrowest, Narrow, Normal, Wide, Widest }

public class ResponsiveHelper
{
Expand All @@ -123,7 +123,7 @@ public class ResponsiveHelper

private readonly List<WeakReference> _callbacks = new();
#if UNO14502_WORKAROUND
private List<IResponsiveCallback> _hardCallbackReferences = new();
private readonly List<IResponsiveCallback> _hardCallbackReferences = new();
#endif

public ResponsiveLayout Layout { get; private set; } = ResponsiveLayout.Create(150, 300, 600, 800, 1080);
Expand Down Expand Up @@ -169,7 +169,7 @@ private void OnWindowSizeChanged(Size size)
continue;
}
#endif
callback.OnSizeChanged(WindowSize, Layout);
callback.OnSizeChanged(this);
}
}
}
Expand All @@ -188,6 +188,26 @@ internal void Register(IResponsiveCallback host)
_callbacks.Add(wr);
}

internal (ResponsiveLayout Layout, Size Size, Layout? Result) ResolveLayout(ResponsiveLayout? layout, IEnumerable<Layout> options)
{
layout ??= Layout;
var result =
options.FirstOrNull(SatisfyLayoutThreshold) ??
options.LastOrNull();

return (layout, WindowSize, result);

bool SatisfyLayoutThreshold(Layout x) => x switch
{
UI.Layout.Narrowest => layout.Narrowest,
UI.Layout.Narrow => layout.Narrow,
UI.Layout.Normal => layout.Normal,
UI.Layout.Wide => layout.Wide,
UI.Layout.Widest => layout.Widest,
_ => double.NaN,
} >= WindowSize.Width;
}

internal static IDisposable UsingDebuggableInstance()
{
UseDebuggableInstance = true;
Expand Down
5 changes: 2 additions & 3 deletions src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,7 @@ static IEnumerable<string> GetDetails(object x)
#region Toolkit Control Details
if (x is ResponsiveView rv)
{
if (rv.ResolvedLayout is { } resolved) yield return $"Resolved={resolved.Layout}";
yield return $"Layout={rv.GetAppliedLayout()}";
yield return $"Responsive: {FormatSize(rv.LastResolved.Size)}@{rv.LastResolved.Layout}->{rv.LastResolved.Result}";
}
#if DEBUG
if (ResponsiveExtension.TrackedInstances.Where(y => y.Owner.Target == x).ToArray() is { Length: > 0 } instances)
Expand All @@ -270,7 +269,7 @@ static IEnumerable<string> GetDetails(object x)
{
if (item.Extension.Target is ResponsiveExtension re)
{
yield return $"{item.Property}@Responsive: {re.LastUsedLayout}->{re.ResolvedValue?.Layout}\\{re.ResolvedValue?.Value}";
yield return $"{item.Property}@Responsive: {FormatSize(re.LastResolved.Size)}@{re.LastResolved.Layout}->{re.LastResolved.Result}\\{re.CurrentValue}";
}
}
}
Expand Down
118 changes: 72 additions & 46 deletions src/Uno.Toolkit.UI/Markup/ResponsiveExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
#endif

using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Windows.Foundation;
using Uno.Extensions;
using Uno.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Linq;



#if IS_WINUI
using Microsoft.UI.Xaml;
Expand Down Expand Up @@ -49,10 +53,10 @@ public partial class ResponsiveExtension : MarkupExtension, IResponsiveCallback
private Type? _propertyType;
private DependencyProperty? _targetProperty;
#endif
internal ResolvedLayout<object?>? ResolvedValue { get; private set; }
#if DEBUG
internal ResponsiveLayout? LastUsedLayout { get; private set; }
#endif

public Layout? CurrentLayout { get; private set; }
internal object? CurrentValue { get; private set; }
internal (ResponsiveLayout Layout, Size Size, Layout? Result) LastResolved { get; private set; }

public ResponsiveExtension()
{
Expand All @@ -62,7 +66,7 @@ public ResponsiveExtension()
/// <inheritdoc/>
protected override object? ProvideValue()
{
this.Log().WarnIfEnabled(() => "The property value, once initially set, cannot be updated due to UWP limitation. Consider upgrading to WinUI, on which the service provider context is exposed through a ProvideValue overload.");
_logger.WarnIfEnabled(() => "The property value, once initially set, cannot be updated due to UWP limitation. Consider upgrading to WinUI, on which the service provider context is exposed through a ProvideValue overload.");
return ResolveValue();
}
#else
Expand All @@ -75,42 +79,6 @@ public ResponsiveExtension()
}
#endif

private object? ResolveValue()
{
var helper = ResponsiveHelper.GetForCurrentView();

return ResolveValue(helper.WindowSize, GetAppliedLayout() ?? helper.Layout);
}

private object? ResolveValue(Size size, ResponsiveLayout layout)
{
var defs = new (double MinWidth, ResolvedLayout<object?> Value)[]
{
(layout.Narrowest, new(nameof(layout.Narrowest), Narrowest)),
(layout.Narrow, new(nameof(layout.Narrow), Narrow)),
(layout.Normal, new(nameof(layout.Normal), Normal)),
(layout.Wide, new(nameof(layout.Wide), Wide)),
(layout.Widest, new(nameof(layout.Widest), Widest)),
}.Where(x => x.Value.Value != null).ToArray();
var match = defs.FirstOrNull(y => y.MinWidth >= size.Width) ?? defs.LastOrNull();
var resolved = match?.Value;

#if DEBUG
LastUsedLayout = layout;
#endif
ResolvedValue = resolved;

var result = resolved?.Value;
#if SUPPORTS_XAML_SERVICE_PROVIDER
if (result != null && _propertyType != null && result.GetType() != _propertyType)
{
result = XamlCastSafe(result, _propertyType);
}
#endif

return result;
}

#if SUPPORTS_XAML_SERVICE_PROVIDER
private void BindToEvents(IXamlServiceProvider serviceProvider)
{
Expand All @@ -123,7 +91,7 @@ private void BindToEvents(IXamlServiceProvider serviceProvider)
_targetProperty = dp;
_propertyType =
#if HAS_UNO // workaround for uno#14719: uno doesn't inject the proper pvtp.Type
target.GetType().GetProperty(pvtp.Name, Public | Instance | FlattenHierarchy)?.PropertyType;
typeof(DependencyProperty).GetProperty("Type", Instance | NonPublic)?.GetValue(dp) as Type;
#else
pvtp.Type;
#endif
Expand Down Expand Up @@ -154,22 +122,50 @@ private void OnTargetLoaded(object sender, RoutedEventArgs e)
// Along the visual tree, we may have a DefaultResponsiveLayout defined in the resources which could cause a different value to be resolved.
// But because in ProvideValue, the target has not been added to the visual tree yet, we cannot access the "full" .resources yet.
// So we need to rectify that here.
target.SetValue(_targetProperty, ResolveValue());
UpdateBindingIfNeeded(forceApplyValue: true);
}
}
#endif

public void OnSizeChanged(Size size, ResponsiveLayout layout)
public void OnSizeChanged(ResponsiveHelper helper) => UpdateBindingIfNeeded(helper);

[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "platform-specific block...")]
private void UpdateBindingIfNeeded(ResponsiveHelper? helper = null, bool forceApplyValue = false)
{
#if SUPPORTS_XAML_SERVICE_PROVIDER
helper ??= ResponsiveHelper.GetForCurrentView();

if (TargetWeakRef?.Target is FrameworkElement target &&
_targetProperty is not null)
{
target.SetValue(_targetProperty, ResolveValue(size, GetAppliedLayout() ?? layout));
var resolved = helper.ResolveLayout(GetAppliedLayout(), GetAvailableLayoutOptions());
if (forceApplyValue || CurrentLayout != resolved.Result)
{
var value = GetValueFor(resolved.Result);

target.SetValue(_targetProperty, value);

CurrentValue = value;
CurrentLayout = resolved.Result;
LastResolved = resolved;
}
}
#endif
}

private object? ResolveValue()
{
var helper = ResponsiveHelper.GetForCurrentView();
var resolved = helper.ResolveLayout(GetAppliedLayout(), GetAvailableLayoutOptions());
var value = GetValueFor(resolved.Result);

CurrentValue = value;
CurrentLayout = resolved.Result;
LastResolved = resolved;

return value;
}

private static object? XamlCastSafe(object value, Type type)
{
try
Expand All @@ -187,6 +183,36 @@ public void OnSizeChanged(Size size, ResponsiveLayout layout)
}
}

private object? GetValueFor(Layout? layout)
{
var value = layout switch
{
UI.Layout.Narrowest => Narrowest,
UI.Layout.Narrow => Narrow,
UI.Layout.Normal => Normal,
UI.Layout.Wide => Wide,
UI.Layout.Widest => Widest,
_ => null,
};
#if SUPPORTS_XAML_SERVICE_PROVIDER
if (value != null && _propertyType != null && value.GetType() != _propertyType)
{
value = XamlCastSafe(value, _propertyType);
}
#endif

return value;
}

private IEnumerable<Layout> GetAvailableLayoutOptions()
{
if (Narrowest != null) yield return UI.Layout.Narrowest;
if (Narrow != null) yield return UI.Layout.Narrow;
if (Normal != null) yield return UI.Layout.Normal;
if (Wide != null) yield return UI.Layout.Wide;
if (Widest != null) yield return UI.Layout.Widest;
}

internal ResponsiveLayout? GetAppliedLayout() =>
Layout ??
#if SUPPORTS_XAML_SERVICE_PROVIDER
Expand Down
Loading