Skip to content

Commit

Permalink
Merge pull request #19387 from unoplatform/dev/dr/hrUpdates
Browse files Browse the repository at this point in the history
fix(hr): Misc updates on HR
  • Loading branch information
jeromelaban authored Jan 31, 2025
2 parents 6694a4d + 245b355 commit e5deea5
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ private void CheckMetadataUpdatesSupport()
|| (buildingInsideVisualStudio && Debugger.IsAttached && OperatingSystem.IsAndroid())
|| (buildingInsideVisualStudio && Debugger.IsAttached && OperatingSystem.IsIOS());


_supportsMetadataUpdates = devServerEnabled || vsEnabled;
_serverMetadataUpdatesEnabled = devServerEnabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private static async IAsyncEnumerable<TMatch> EnumerateHotReloadInstances<TMatch
}
}
}
#if __IOS__ || __ANDROID
#if __IOS__ || __ANDROID__
#if __IOS__
else if (instance is UIView nativeView)
#elif __ANDROID__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -20,6 +21,7 @@
using Microsoft.UI.Xaml.Media;
using Uno.Diagnostics.UI;
using Uno.Threading;
using static Uno.UI.RemoteControl.HotReload.MetadataUpdater.ElementUpdateAgent;

#if HAS_UNO_WINUI
using _WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs;
Expand Down Expand Up @@ -119,82 +121,108 @@ private static async Task ReloadWithUpdatedTypes(HotReloadClientOperation? hrOp,

var capturedStates = new Dictionary<string, Dictionary<string, object>>();

static int GetSubClassDepth(Type? type, Type baseType)
{
var count = 0;
if (type == baseType)
{
return 0;
}
for (; type != null; type = type.BaseType)
{
count++;
if (type == baseType)
{
return count;
}
}
return -1;
}

var isCapturingState = true;
var treeIterator = EnumerateHotReloadInstances(
window.Content,
async (fe, key) =>
window.Content,
async (fe, key) =>
{
// Get the original type of the element, in case it's been replaced
var liveType = fe.GetType();
var originalType = liveType.GetOriginalType() ?? fe.GetType();

// Get the handler for the type specified
// Since we're only interested in handlers for specific element types
// we exclude those registered for "object". Handlers that want to run
// for all element types should register for FrameworkElement instead
ImmutableArray<ElementUpdateHandlerActions> handlers =
[
..from handler in handlerActions
let depth = GetSubClassDepth(originalType, handler.Key)
where depth is not -1 && handler.Key != typeof(object)
orderby depth descending
select handler.Value
];

// Get the replacement type, or null if not replaced
var mappedType = originalType.GetMappedType();
foreach (var handler in handlers)
{
// Get the original type of the element, in case it's been replaced
var liveType = fe.GetType();
var originalType = liveType.GetOriginalType() ?? fe.GetType();

// Get the handler for the type specified
// Since we're only interested in handlers for specific element types
// we exclude those registered for "object". Handlers that want to run
// for all element types should register for FrameworkElement instead
var handler = (from h in handlerActions
where (originalType == h.Key ||
originalType.IsSubclassOf(h.Key)) &&
h.Key != typeof(object)
select h.Value).FirstOrDefault();

// Get the replacement type, or null if not replaced
var mappedType = originalType.GetMappedType();

if (handler is not null)
if (!capturedStates.TryGetValue(key, out var dict))
{
if (!capturedStates.TryGetValue(key, out var dict))
{
dict = new();
}
if (isCapturingState)
{
handler.CaptureState(fe, dict, updatedTypes);
if (dict.Any())
{
capturedStates[key] = dict;
}
}
else
{
await handler.RestoreState(fe, dict, updatedTypes);
}
dict = new();
}

if (updatedTypes.Contains(liveType))
if (isCapturingState)
{
// This may happen if one of the nested types has been hot reloaded, but not the type itself.
// For instance, a DataTemplate in a resource dictionary may mark the type as updated in `updatedTypes`
// but it will not be considered as a new type even if "CreateNewOnMetadataUpdate" was set.

return (fe, null, liveType);
handler.CaptureState(fe, dict, updatedTypes);
if (dict.Any())
{
capturedStates[key] = dict;
}
}
else
{
return (handler is not null || mappedType is not null) ? (fe, handler, mappedType) : default;
await handler.RestoreState(fe, dict, updatedTypes);
}
},
parentKey: default);
}

if (updatedTypes.Contains(liveType))
{
// This may happen if one of the nested types has been hot reloaded, but not the type itself.
// For instance, a DataTemplate in a resource dictionary may mark the type as updated in `updatedTypes`
// but it will not be considered as a new type even if "CreateNewOnMetadataUpdate" was set.

return (fe, ImmutableArray<ElementUpdateHandlerActions>.Empty, liveType);
}
else
{
return !handlers.IsDefaultOrEmpty || mappedType is not null
? (fe, handlers, mappedType)
: default;
}
},
parentKey: default);

// Forced iteration to capture all state before doing ui update
var instancesToUpdate = await treeIterator.ToArrayAsync();

// Iterate through the visual tree and either invoke ElementUpdate,
// or replace the element with a new one
foreach (var (element, elementHandler, elementMappedType) in instancesToUpdate)
foreach (var (element, elementHandlers, elementMappedType) in instancesToUpdate)
{
// Action: ElementUpdate
// This is invoked for each existing element that is in the tree that needs to be replaced
elementHandler?.ElementUpdate(element, updatedTypes);

if (elementMappedType is not null)
foreach (var elementHandler in elementHandlers)
{
if (_log.IsEnabled(LogLevel.Trace))
elementHandler?.ElementUpdate(element, updatedTypes);

if (elementMappedType is not null)
{
_log.Error($"Updating element [{element}] to [{elementMappedType}]");
}
if (_log.IsEnabled(LogLevel.Trace))
{
_log.Error($"Updating element [{element}] to [{elementMappedType}]");
}

ReplaceViewInstance(element, elementMappedType, elementHandler);
ReplaceViewInstance(element, elementMappedType, elementHandler);
}
}
}

Expand Down
20 changes: 17 additions & 3 deletions src/Uno.UI.RemoteControl/RemoteControlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints =
{
AppType = appType;
_status = new StatusSink(this);
var loggedError = false;

// Environment variables are the first priority as they are used by runtime tests engine to test hot-reload.
// They should be considered as the default values and in any case they must take precedence over the assembly-provided values.
Expand All @@ -169,15 +170,15 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints =

// Get the addresses from the assembly attributes set by the code-gen in debug (i.e. from the IDE)
if (_serverAddresses is null or { Length: 0 }
&& appType.Assembly.GetCustomAttributes(typeof(ServerEndpointAttribute), false) is ServerEndpointAttribute[] embeddedEndpoints)
&& appType.Assembly.GetCustomAttributes(typeof(ServerEndpointAttribute), false) is ServerEndpointAttribute[] { Length: > 0 } embeddedEndpoints)
{
IEnumerable<(string endpoint, int port)> GetAddresses()
{
foreach (var endpoint in embeddedEndpoints)
{
if (endpoint.Port is 0 && !Uri.TryCreate(endpoint.Endpoint, UriKind.Absolute, out _))
{
this.Log().LogError($"Failed to get remote control server port from the IDE for endpoint {endpoint.Endpoint}.");
this.Log().LogInfo($"Failed to get dev-server port from the IDE for endpoint {endpoint.Endpoint}.");
}
else
{
Expand All @@ -187,6 +188,14 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints =
}

_serverAddresses = GetAddresses().ToArray();
if (_serverAddresses is { Length: 0 })
{
this.Log().LogError(
"Some endpoint for uno's dev-server has been configured in your application, but all are invalid (port is missing?). "
+ "This can usually be fixed with a **rebuild** of your application. "
+ "If not, make sure you have the latest version of the uno's extensions installed in your IDE and restart your IDE.");
loggedError = true;
}
}

if (_serverAddresses is null or { Length: 0 })
Expand All @@ -203,7 +212,12 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints =

if (_serverAddresses is null or { Length: 0 })
{
this.Log().LogError("Failed to get any remote control server endpoint from the IDE.");
if (!loggedError)
{
this.Log().LogError(
"Failed to get any valid dev-server endpoint from the IDE."
+ "Make sure you have the latest version of the uno's extensions installed in your IDE and restart your IDE.");
}

_connection = Task.FromResult<Connection?>(null);
_status.Report(ConnectionState.NoServer);
Expand Down

0 comments on commit e5deea5

Please sign in to comment.