Skip to content

Commit

Permalink
feat: Add ability to pause and result hot reload
Browse files Browse the repository at this point in the history
  • Loading branch information
nickrandolph committed Sep 29, 2023
1 parent 29c589b commit 91e8c8d
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
using System.Threading;
using static Windows.UI.Xaml.Markup.Reader.XamlConstants;

namespace Uno.UI.RemoteControl.HotReload
{
Expand Down Expand Up @@ -170,56 +171,108 @@ static int[] ReadIntArray(BinaryReader binaryReader)
return values;
}
#endif
private static void ReloadWithUpdatedTypes(Type[] updatedTypes)

private static int _isReloading;

private static async void ReloadWithUpdatedTypes(Type[] updatedTypes)
{
if (updatedTypes.Length == 0)
if (_log.IsEnabled(LogLevel.Trace))
{
_log.Trace($"ReloadWithUpdatedTypes - start");
}

if (Interlocked.CompareExchange(ref _isReloading, 1, 0) == 1)
{
return;
}
try
{
var task = TypeMappingHelper.WaitToReload();
if (task != null)
{
// If a Task is returned, it means reloading has been paused
// The task will complete when reload should resume
await task;
}
}
finally
{
Interlocked.Exchange(ref _isReloading, 0);
}

var handlerActions = _instance?.ElementAgent?.ElementHandlerActions;
try
{

// Action: BeforeVisualTreeUpdate
// This is called before the visual tree is updated
handlerActions?.Do(h => h.Value.BeforeVisualTreeUpdate(updatedTypes));
var handlerActions = _instance?.ElementAgent?.ElementHandlerActions;

// Iterate through the visual tree and either invole ElementUpdate,
// or replace the element with a new one
foreach (
var (element, elementHandler, elementMappedType) in
EnumerateHotReloadInstances(
Window.Current.Content,
fe =>
{
// Get the original type of the element, in case it's been replaced
var originalType = fe.GetType().GetOriginalType() ?? fe.GetType();
if (_log.IsEnabled(LogLevel.Trace))
{
if (handlerActions is null)
{
_log.Trace($"ReloadWithUpdatedTypes - handlerActions is null");
}
else
{
_log.Trace($"ReloadWithUpdatedTypes - There are {handlerActions.Count} ElementHandlerActions registered");
}
}

// Get the handler for the type specified
var handler = (from h in handlerActions
where originalType == h.Key ||
originalType.IsSubclassOf(h.Key)
select h.Value).FirstOrDefault();
// Action: BeforeVisualTreeUpdate
// This is called before the visual tree is updated
_ = handlerActions?.Do(h => h.Value.BeforeVisualTreeUpdate(updatedTypes)).ToArray();

// Iterate through the visual tree and either invole ElementUpdate,
// or replace the element with a new one
foreach (
var (element, elementHandler, elementMappedType) in
EnumerateHotReloadInstances(
Window.Current.Content,
fe =>
{
// Get the original type of the element, in case it's been replaced
var originalType = fe.GetType().GetOriginalType() ?? fe.GetType();
// Get the handler for the type specified
var handler = (from h in handlerActions
where originalType == h.Key ||
originalType.IsSubclassOf(h.Key)
select h.Value).FirstOrDefault();
// Get the replacement type, or null if not replaced
var mappedType = originalType.GetMappedType();
return (handler is not null || mappedType is not null) ? (fe, handler, mappedType) : default;
},
enumerateChildrenAfterMatch: true))
{

// Get the replacement type, or null if not replaced
var mappedType = originalType.GetMappedType();
// Action: ElementUpdate
// This is invoked for each existing element that is in the tree that needs to be replaced
elementHandler?.ElementUpdate(element, updatedTypes);

return (handler is not null || mappedType is not null) ? (fe, handler, mappedType) : default;
},
enumerateChildrenAfterMatch: true))
{
if (elementMappedType is not null)
{
ReplaceViewInstance(element, elementMappedType, elementHandler);
}
}

// Action: ElementUpdate
// This is invoked for each existing element that is in the tree that needs to be replaced
elementHandler?.ElementUpdate(element, updatedTypes);
// Action: AfterVisualTreeUpdate
_ = handlerActions?.Do(h => h.Value.AfterVisualTreeUpdate(updatedTypes)).ToArray();

if (elementMappedType is not null)
if (_log.IsEnabled(LogLevel.Trace))
{
_log.Trace($"ReloadWithUpdatedTypes - end");
}
}
catch (Exception ex)
{
if (_log.IsEnabled(LogLevel.Error))
{
ReplaceViewInstance(element, elementMappedType, elementHandler);
_log.Error($"ReloadWithUpdatedTypes error", ex);
}
throw;
}

// Action: AfterVisualTreeUpdate
handlerActions?.Do(h => h.Value.AfterVisualTreeUpdate(updatedTypes));
}

private static void ReplaceViewInstance(UIElement instance, Type replacementType, ElementUpdateAgent.ElementUpdateHandlerActions? handler = default, Type[]? updatedTypes = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,21 @@ internal void GetElementHandlerActions(

if (GetUpdateMethod(handlerType, nameof(ElementUpdateHandlerActions.BeforeVisualTreeUpdate)) is MethodInfo beforeVisualTreeUpdate)
{
updateActions.BeforeVisualTreeUpdate = CreateAction(beforeVisualTreeUpdate);
_log($"Adding handler for {nameof(updateActions.BeforeVisualTreeUpdate)} for {handlerType}");
updateActions.BeforeVisualTreeUpdate = CreateAction(beforeVisualTreeUpdate, handlerType);
methodFound = true;
}

if (GetUpdateMethod(handlerType, nameof(ElementUpdateHandlerActions.AfterVisualTreeUpdate)) is MethodInfo afterVisualTreeUpdate)
{
updateActions.AfterVisualTreeUpdate = CreateAction(afterVisualTreeUpdate);
_log($"Adding handler for {nameof(updateActions.AfterVisualTreeUpdate)} for {handlerType}");
updateActions.AfterVisualTreeUpdate = CreateAction(afterVisualTreeUpdate, handlerType);
methodFound = true;
}

if (GetHandlerMethod(handlerType, nameof(ElementUpdateHandlerActions.ElementUpdate), new[] { typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo elementUpdate)
{
_log($"Adding handler for {nameof(updateActions.ElementUpdate)} for {handlerType}");
updateActions.ElementUpdate = CreateHandlerAction<Action<FrameworkElement, Type[]?>>(elementUpdate);
methodFound = true;
}
Expand All @@ -167,6 +170,7 @@ internal void GetElementHandlerActions(
nameof(ElementUpdateHandlerActions.BeforeElementReplaced),
new[] { typeof(FrameworkElement), typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo beforeElementReplaced)
{
_log($"Adding handler for {nameof(updateActions.BeforeElementReplaced)} for {handlerType}");
updateActions.BeforeElementReplaced = CreateHandlerAction<Action<FrameworkElement, FrameworkElement, Type[]?>>(beforeElementReplaced);
methodFound = true;
}
Expand All @@ -176,6 +180,7 @@ internal void GetElementHandlerActions(
nameof(ElementUpdateHandlerActions.AfterElementReplaced),
new[] { typeof(FrameworkElement), typeof(FrameworkElement), typeof(Type[]) }) is MethodInfo afterElementReplaced)
{
_log($"Adding handler for {nameof(updateActions.AfterElementReplaced)} for {handlerType}");
updateActions.AfterElementReplaced = CreateHandlerAction<Action<FrameworkElement, FrameworkElement, Type[]?>>(afterElementReplaced);
methodFound = true;
}
Expand Down Expand Up @@ -218,18 +223,19 @@ internal void GetElementHandlerActions(
return null;
}

private Action<Type[]?> CreateAction(MethodInfo update)
private Action<Type[]?> CreateAction(MethodInfo update, Type handlerType)
{
var action = CreateHandlerAction<Action<Type[]?>>(update);
return types =>
{
try
{
action(types);
_log($"Invoking '{update.Name}' on {handlerType.Name} (Action null: {action is null})");
action?.Invoke(types);
}
catch (Exception ex)
{
_log($"Exception from '{action}': {ex}");
_log($"Exception from '{update.Name}' on {handlerType.Name}: {ex}");
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection.Metadata;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.Extensions;
using Uno.UI.Helpers;
using Uno.UI.RuntimeTests.Tests.HotReload.Frame.HRApp.Tests;
using Uno.UI.RuntimeTests.Tests.HotReload.Frame.Pages;

Expand All @@ -12,14 +13,25 @@ namespace Uno.UI.RuntimeTests.Tests.HotReload.Frame.HRApp.Tests;

public static class TestingUpdateHandler
{
private static TaskCompletionSource _visualTreeUpdateCompletion = new TaskCompletionSource();
public static void BeforeVisualTreeUpdate(Type[]? updatedTypes)
{
typeof(TestingUpdateHandler).Log().LogWarning("--------------------------Before Visaul Tree Update");
//throw new Exception("BeforeVisualTreeUpdate");
typeof(TestingUpdateHandler).Log().LogWarning("--------------------------Before Visual Tree Update");
}

public static void AfterVisualTreeUpdate(Type[]? updatedTypes)
{
typeof(TestingUpdateHandler).Log().LogWarning("--------------------------After Visaul Tree Update");
//throw new Exception("AfterVisualTreeUpdate");
typeof(TestingUpdateHandler).Log().LogWarning("--------------------------After Visual Tree Update");
var oldCompletion = _visualTreeUpdateCompletion;
_visualTreeUpdateCompletion = new TaskCompletionSource();
oldCompletion.TrySetResult();
}

public static async Task WaitForVisualTreeUpdate()
{
await _visualTreeUpdateCompletion.Task;
}
}

Expand Down Expand Up @@ -66,7 +78,7 @@ public async Task Check_NoChange_Page1()
/// Change Page1
/// </summary>
[TestMethod]
public async Task Check_Can_Change_Page1()
public async Task Check_Can_Change_Page1_NoPause()
{
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;

Expand Down Expand Up @@ -111,19 +123,21 @@ public async Task Check_Can_Change_Page1_Pause_HR()
await frame.ValidateFirstTextBlockOnCurrentPageText(FirstPageTextBlockOriginalText);

// Pause HR
TypeMappingHelper.PauseReloading();

// Check the text of the TextBlock is the same even after a HR change (since HR is paused)
await HotReloadHelper.UpdateServerFileAndRevert<HR_Frame_Pages_Page1>(
FirstPageTextBlockOriginalText,
FirstPageTextBlockChangedText,
async () =>
{
await frame.ValidateFirstTextBlockOnCurrentPageText(FirstPageTextBlockChangedText);
await frame.ValidateFirstTextBlockOnCurrentPageText(FirstPageTextBlockOriginalText);
//await frame.ValidateFirstTextBlockOnCurrentPageText(FirstPageTextBlockChangedText);
},
ct);

// Resume HR
TypeMappingHelper.ResumeReloading();

// Check that the text has been updated
await frame.ValidateFirstTextBlockOnCurrentPageText(FirstPageTextBlockOriginalText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
mc:Ignorable="d">

<StackPanel>
<TextBlock Text="First page (changed) (changed) (changed)"
<TextBlock Text="First page"
x:Name="FirstPageTextBlock" />
<TextBlock Text="{Binding TitleText}" />
<local:HR_Frame_Pages_UC1 />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Uno.UI.RemoteControl;
using Uno.UI.RemoteControl.HotReload.Messages;
using Uno.UI.RemoteControl.HotReload.MetadataUpdater;
using Uno.UI.RuntimeTests.Tests.HotReload.Frame.HRApp.Tests;
using Uno.UI.RuntimeTests.Tests.HotReload.Frame.Pages;
using Windows.UI.Xaml;

Expand Down Expand Up @@ -36,16 +37,39 @@ public static async Task UpdateServerFile<T>(string originalText, string replace

await RemoteControlClient.Instance.SendMessage(message);

var cts = new TaskCompletionSource();
ct.Register(() => cts.TrySetCanceled());

void UpdateReceived(object? sender, object? args) => cts.TrySetResult();
var reloadWaiter = TypeMappingHelper.WaitToReload();
if (reloadWaiter != null)
{
// Reloads are paused, so don't wait for any update
return;
}
await TestingUpdateHandler.WaitForVisualTreeUpdate().WaitAsync(ct);
}

public static void UpdateServerFileFireAndForget<T>(string originalText, string replacementText)
where T : FrameworkElement, new()
{
if (RemoteControlClient.Instance is null)
{
return;
}

MetadataUpdaterHelper.MetadataUpdated += UpdateReceived;
var message = new T().CreateUpdateFileMessage(
originalText: originalText,
replacementText: replacementText);

await cts.Task;
if (message is null)
{
return;
}
Task.Run(() =>
{
_ = RemoteControlClient.Instance.SendMessage(message);
});
}


public static async Task UpdateServerFileAndRevert<T>(
string originalText,
string replacementText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,11 @@ public static async Task ValidateTextBlockOnCurrentPageText(this Windows.UI.Xaml

TextBlock? firstText = null;

while (sw.Elapsed < timeout)
{
firstText = element
.EnumerateDescendants()
.OfType<TextBlock>()
.Skip(index)
.FirstOrDefault();

if (firstText?.Text == expectedText)
{
typeof(UIElementExtensions).Log().LogWarning($"$$$$$$$$$$$$$$$$ Text matches - {expectedText}");
break;
}

await Task.Delay(100);
}
firstText = element
.EnumerateDescendants()
.OfType<TextBlock>()
.Skip(index)
.FirstOrDefault();

typeof(UIElementExtensions).Log().LogWarning($"$$$$$$$$$$$$$$$$ After wait - Null: {firstText is null} Match: {expectedText == firstText?.Text} - {expectedText}");
Assert.IsNotNull(firstText);
Expand Down
Loading

0 comments on commit 91e8c8d

Please sign in to comment.