From 546ffbe32e4efe4976ad02704b3a5d6ac414b499 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Fri, 27 Oct 2023 11:16:01 +0100 Subject: [PATCH 01/33] bootstrap alternative modules dependency provider --- .../FileDependenciesProvider.cs | 46 + .../IModuleDependenciesProvider.cs | 7 + ReactViewControl/IViewModule.cs | 79 +- ReactViewControl/ReactView.cs | 496 +++---- ReactViewControl/ReactViewRender.cs | 1254 ++++++++--------- ReactViewControl/ViewModuleContainer.cs | 236 ++-- Sample.Avalonia/ExtendedReactView.cs | 58 +- 7 files changed, 1104 insertions(+), 1072 deletions(-) create mode 100644 ReactViewControl.Avalonia/FileDependenciesProvider.cs create mode 100644 ReactViewControl.Avalonia/IModuleDependenciesProvider.cs diff --git a/ReactViewControl.Avalonia/FileDependenciesProvider.cs b/ReactViewControl.Avalonia/FileDependenciesProvider.cs new file mode 100644 index 00000000..ad546507 --- /dev/null +++ b/ReactViewControl.Avalonia/FileDependenciesProvider.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Linq; +using WebViewControl; + +namespace ReactViewControl { + class FileDependenciesProvider : IModuleDependenciesProvider { + + private const string JsEntryFileExtension = ".js.entry"; + private const string CssEntryFileExtension = ".css.entry"; + + private readonly string sourcePath; + + public FileDependenciesProvider(string sourcePath) { + DependencyJsSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(JsEntryFileExtension)); + CssSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(CssEntryFileExtension)); + this.sourcePath = sourcePath; + } + + public string[] GetCssDependencies(string filename) => DependencyJsSourcesCache.Value; + + public string[] GetJsDependencies(string filename) => CssSourcesCache.Value; + + private Lazy DependencyJsSourcesCache { get; } + private Lazy CssSourcesCache { get; } + + private string[] GetDependenciesFromEntriesFile(string extension) { + var entriesFilePath = Path.Combine(Path.GetDirectoryName(sourcePath), Path.GetFileNameWithoutExtension(sourcePath) + extension); + var resource = entriesFilePath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + + using (var stream = GetResourceStream(resource)) { + if (stream != null) { + using (var reader = new StreamReader(stream)) { + var allEntries = reader.ReadToEnd(); + if (allEntries != null && allEntries != string.Empty) { + return allEntries.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + } + } + } + } + return new string[0]; + } + + private Stream GetResourceStream(string[] resource) => ResourcesManager.TryGetResourceWithFullPath(resource.First(), resource); + } +} \ No newline at end of file diff --git a/ReactViewControl.Avalonia/IModuleDependenciesProvider.cs b/ReactViewControl.Avalonia/IModuleDependenciesProvider.cs new file mode 100644 index 00000000..bcec00a7 --- /dev/null +++ b/ReactViewControl.Avalonia/IModuleDependenciesProvider.cs @@ -0,0 +1,7 @@ +namespace ReactViewControl { + public interface IModuleDependenciesProvider { + string[] GetCssDependencies(string filename); + + string[] GetJsDependencies(string filename); + } +} \ No newline at end of file diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index 77b54a01..ba2ac6d7 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -1,40 +1,39 @@ -using System; -using System.Collections.Generic; - -namespace ReactViewControl { - - public interface IViewModule { - - string MainJsSource { get; } - - string[] DependencyJsSources { get; } - - string[] CssSources { get; } - - string NativeObjectName { get; } - - string Name { get; } - - string Source { get; } - - object CreateNativeObject(); - - string[] Events { get; } - - KeyValuePair[] PropertiesValues { get; } - - void Bind(IFrame frame, IChildViewHost host = null); - - event CustomResourceRequestedEventHandler CustomResourceRequested; - - T WithPlugin(); - - void Load(); - - bool AddChildView(IViewModule childView, string frameName); - - T GetOrAddChildView(string frameName) where T : IViewModule, new(); - - ReactView Host { get; } - } -} +using System.Collections.Generic; + +namespace ReactViewControl { + + public interface IViewModule { + + string MainJsSource { get; } + + string[] DependencyJsSources { get; } + + string[] CssSources { get; } + + string NativeObjectName { get; } + + string Name { get; } + + string Source { get; } + + object CreateNativeObject(); + + string[] Events { get; } + + KeyValuePair[] PropertiesValues { get; } + + void Bind(IFrame frame, IChildViewHost host = null); + + event CustomResourceRequestedEventHandler CustomResourceRequested; + + T WithPlugin(); + + void Load(); + + bool AddChildView(IViewModule childView, string frameName); + + T GetOrAddChildView(string frameName) where T : IViewModule, new(); + + ReactView Host { get; } + } +} diff --git a/ReactViewControl/ReactView.cs b/ReactViewControl/ReactView.cs index 7393b786..beaedadf 100644 --- a/ReactViewControl/ReactView.cs +++ b/ReactViewControl/ReactView.cs @@ -1,249 +1,249 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using WebViewControl; - -namespace ReactViewControl { - - public delegate void ResourceRequestedEventHandler(ResourceHandler resourceHandler); - - public delegate Resource CustomResourceRequestedEventHandler(string resourceKey, params string[] options); - - public abstract partial class ReactView : IDisposable { - - private static Dictionary CachedViews { get; } = new Dictionary(); - - private ReactViewRender View { get; } - - private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { - ReactViewRender InnerCreateView() { - var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.DevServerURI); - if (factory.ShowDeveloperTools) { - view.ShowDeveloperTools(); - } - return view; - } - - if (factory.EnableViewPreload) { - var factoryType = factory.GetType(); - // check if we have a view cached for the current factory - if (CachedViews.TryGetValue(factoryType, out var cachedView)) { - CachedViews.Remove(factoryType); - } - - // create a new view in the background and put it in the cache - AsyncExecuteInUI(() => { - if (!CachedViews.ContainsKey(factoryType)) { - CachedViews.Add(factoryType, InnerCreateView()); - } - }, lowPriority: true); - - if (cachedView != null) { - return cachedView; - } - } - - return InnerCreateView(); - } - - protected ReactView(IViewModule mainModule) { - View = CreateReactViewInstance(Factory); - - View.Host = this; - MainModule = mainModule; - // bind main module (this is needed so that the plugins are available right away) - View.BindComponent(mainModule); - - ExtraInitialize(); - } - - partial void ExtraInitialize(); - - ~ReactView() { - Dispose(); - } - - public void Dispose() { - InnerDispose(); - View?.Dispose(); - GC.SuppressFinalize(this); - } - - protected virtual void InnerDispose() { } - - /// - /// Factory used to configure the initial properties of the control. - /// - protected virtual ReactViewFactory Factory => new ReactViewFactory(); - - protected void RefreshDefaultStyleSheet() { - View.DefaultStyleSheet = Factory.DefaultStyleSheet; - AsyncExecuteInUI(() => CachedViews.Remove(Factory.GetType()), lowPriority: true); - } - - /// - /// Tries to loads the main component. - /// - protected void TryLoadComponent() { - TryLoadComponent(true); - } - - /// - /// Tries to loads the main component. - /// - /// Tries to initialize underlying view if wasn't yet. - private void TryLoadComponent(bool ensureViewInitialized) { - if (View.IsMainComponentLoaded) { - return; - } - - if (ensureViewInitialized) { - // we're performing an explicit load and view has not been initialized - // try initializing it - AsyncExecuteInUI(() => View.EnsureInitialized(), lowPriority: false); - } - - View.LoadComponent(MainModule); - } - - /// - /// Retrieves the specified plugin module instance. - /// - /// Type of the plugin to retrieve. - /// - public T WithPlugin() { - return View.WithPlugin(); - } - - /// - /// Enables or disables debug mode. - /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details - /// when a resource fails to load. - /// - public bool EnableDebugMode { get => View.EnableDebugMode; set => View.EnableDebugMode = value; } - - /// - /// True when the main component has been rendered. - /// - public bool IsReady => View.IsReady; - - /// - /// Gets or sets the control zoom percentage (1 = 100%) - /// - public double ZoomPercentage { get => View.ZoomPercentage; set => View.ZoomPercentage = value; } - - /// - /// Event fired when the component is rendered and ready for interaction. - /// - public event Action Ready { - add { View.Ready += value; } - remove { View.Ready -= value; } - } - - /// - /// Event fired when an async exception occurs (eg: executing javascript) - /// - public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { - add { View.UnhandledAsyncException += value; } - remove { View.UnhandledAsyncException -= value; } - } - - /// - /// Event fired when a resource fails to load. - /// - public event ResourceLoadFailedEventHandler ResourceLoadFailed { - add { View.ResourceLoadFailed += value; } - remove { View.ResourceLoadFailed -= value; } - } - - /// - /// Handle embedded resource requests. You can use this event to change the resource being loaded. - /// - public event ResourceRequestedEventHandler EmbeddedResourceRequested { - add { View.EmbeddedResourceRequested += value; } - remove { View.EmbeddedResourceRequested -= value; } - } - - /// - /// Handle custom resource requests. Use this event to load the resource based on the provided key. - /// This handler will be called before the frame handler. - /// - public event CustomResourceRequestedEventHandler CustomResourceRequested { - add { View.CustomResourceRequested += value; } - remove { View.CustomResourceRequested -= value; } - } - - /// - /// Handle external resource requests. - /// Call to handle the request in an async way. - /// - public event ResourceRequestedEventHandler ExternalResourceRequested { - add { View.ExternalResourceRequested += value; } - remove { View.ExternalResourceRequested -= value; } - } - - /// - /// Handle drag of files. Use this event to get the full path of the files being dragged. - /// - public event FilesDraggingEventHandler FilesDragging { - add { View.FilesDragging += value; } - remove { View.FilesDragging -= value; } - } - - /// - /// Handle drag of text. Use this event to get the text content being dragged. - /// - public event TextDraggingEventHandler TextDragging { - add { View.TextDragging += value; } - remove { View.TextDragging -= value; } - } - - /// - /// Opens the developer tools. - /// - public void ShowDeveloperTools() { - View.ShowDeveloperTools(); - } - - /// - /// Closes the developer tools. - /// - public void CloseDeveloperTools() { - View.CloseDeveloperTools(); - } - - /// - /// View module of this control. - /// - protected IViewModule MainModule { get; } - - /// - /// Number of preloaded views that are mantained in cache for each view. - /// Components with different property values are stored in different cache entries. - /// Defaults to 6. - /// - public static int PreloadedCacheEntriesSize { get; set; } = 6; - - /// - /// Gets the edition commands. - /// - public EditCommands EditCommands => View.EditCommands; - - /// - /// Called when executing a native method. - /// - protected virtual Task OnNativeMethodCalled(Func nativeMethod) => Task.FromResult(nativeMethod()); - - /// - /// Called before executing/evaluating a JS method - /// - protected virtual void OnBeforeExecuteMethod() { } - - internal void HandledBeforeExecuteMethod() => OnBeforeExecuteMethod(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Task CallNativeMethod(Func nativeMethod) => OnNativeMethodCalled(nativeMethod); - } +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using WebViewControl; + +namespace ReactViewControl { + + public delegate void ResourceRequestedEventHandler(ResourceHandler resourceHandler); + + public delegate Resource CustomResourceRequestedEventHandler(string resourceKey, params string[] options); + + public abstract partial class ReactView : IDisposable { + + private static Dictionary CachedViews { get; } = new Dictionary(); + + private ReactViewRender View { get; } + + private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { + ReactViewRender InnerCreateView() { + var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.DevServerURI); + if (factory.ShowDeveloperTools) { + view.ShowDeveloperTools(); + } + return view; + } + + if (factory.EnableViewPreload) { + var factoryType = factory.GetType(); + // check if we have a view cached for the current factory + if (CachedViews.TryGetValue(factoryType, out var cachedView)) { + CachedViews.Remove(factoryType); + } + + // create a new view in the background and put it in the cache + AsyncExecuteInUI(() => { + if (!CachedViews.ContainsKey(factoryType)) { + CachedViews.Add(factoryType, InnerCreateView()); + } + }, lowPriority: true); + + if (cachedView != null) { + return cachedView; + } + } + + return InnerCreateView(); + } + + protected ReactView(IViewModule mainModule) { + View = CreateReactViewInstance(Factory); + + View.Host = this; + MainModule = mainModule; + // bind main module (this is needed so that the plugins are available right away) + View.BindComponent(mainModule); + + ExtraInitialize(); + } + + partial void ExtraInitialize(); + + ~ReactView() { + Dispose(); + } + + public void Dispose() { + InnerDispose(); + View?.Dispose(); + GC.SuppressFinalize(this); + } + + protected virtual void InnerDispose() { } + + /// + /// Factory used to configure the initial properties of the control. + /// + protected virtual ReactViewFactory Factory => new ReactViewFactory(); + + protected void RefreshDefaultStyleSheet() { + View.DefaultStyleSheet = Factory.DefaultStyleSheet; + AsyncExecuteInUI(() => CachedViews.Remove(Factory.GetType()), lowPriority: true); + } + + /// + /// Tries to loads the main component. + /// + protected void TryLoadComponent() { + TryLoadComponent(true); + } + + /// + /// Tries to loads the main component. + /// + /// Tries to initialize underlying view if wasn't yet. + private void TryLoadComponent(bool ensureViewInitialized) { + if (View.IsMainComponentLoaded) { + return; + } + + if (ensureViewInitialized) { + // we're performing an explicit load and view has not been initialized + // try initializing it + AsyncExecuteInUI(() => View.EnsureInitialized(), lowPriority: false); + } + + View.LoadComponent(MainModule); + } + + /// + /// Retrieves the specified plugin module instance. + /// + /// Type of the plugin to retrieve. + /// + public T WithPlugin() { + return View.WithPlugin(); + } + + /// + /// Enables or disables debug mode. + /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details + /// when a resource fails to load. + /// + public bool EnableDebugMode { get => View.EnableDebugMode; set => View.EnableDebugMode = value; } + + /// + /// True when the main component has been rendered. + /// + public bool IsReady => View.IsReady; + + /// + /// Gets or sets the control zoom percentage (1 = 100%) + /// + public double ZoomPercentage { get => View.ZoomPercentage; set => View.ZoomPercentage = value; } + + /// + /// Event fired when the component is rendered and ready for interaction. + /// + public event Action Ready { + add { View.Ready += value; } + remove { View.Ready -= value; } + } + + /// + /// Event fired when an async exception occurs (eg: executing javascript) + /// + public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { + add { View.UnhandledAsyncException += value; } + remove { View.UnhandledAsyncException -= value; } + } + + /// + /// Event fired when a resource fails to load. + /// + public event ResourceLoadFailedEventHandler ResourceLoadFailed { + add { View.ResourceLoadFailed += value; } + remove { View.ResourceLoadFailed -= value; } + } + + /// + /// Handle embedded resource requests. You can use this event to change the resource being loaded. + /// + public event ResourceRequestedEventHandler EmbeddedResourceRequested { + add { View.EmbeddedResourceRequested += value; } + remove { View.EmbeddedResourceRequested -= value; } + } + + /// + /// Handle custom resource requests. Use this event to load the resource based on the provided key. + /// This handler will be called before the frame handler. + /// + public event CustomResourceRequestedEventHandler CustomResourceRequested { + add { View.CustomResourceRequested += value; } + remove { View.CustomResourceRequested -= value; } + } + + /// + /// Handle external resource requests. + /// Call to handle the request in an async way. + /// + public event ResourceRequestedEventHandler ExternalResourceRequested { + add { View.ExternalResourceRequested += value; } + remove { View.ExternalResourceRequested -= value; } + } + + /// + /// Handle drag of files. Use this event to get the full path of the files being dragged. + /// + public event FilesDraggingEventHandler FilesDragging { + add { View.FilesDragging += value; } + remove { View.FilesDragging -= value; } + } + + /// + /// Handle drag of text. Use this event to get the text content being dragged. + /// + public event TextDraggingEventHandler TextDragging { + add { View.TextDragging += value; } + remove { View.TextDragging -= value; } + } + + /// + /// Opens the developer tools. + /// + public void ShowDeveloperTools() { + View.ShowDeveloperTools(); + } + + /// + /// Closes the developer tools. + /// + public void CloseDeveloperTools() { + View.CloseDeveloperTools(); + } + + /// + /// View module of this control. + /// + protected IViewModule MainModule { get; } + + /// + /// Number of preloaded views that are mantained in cache for each view. + /// Components with different property values are stored in different cache entries. + /// Defaults to 6. + /// + public static int PreloadedCacheEntriesSize { get; set; } = 6; + + /// + /// Gets the edition commands. + /// + public EditCommands EditCommands => View.EditCommands; + + /// + /// Called when executing a native method. + /// + protected virtual Task OnNativeMethodCalled(Func nativeMethod) => Task.FromResult(nativeMethod()); + + /// + /// Called before executing/evaluating a JS method + /// + protected virtual void OnBeforeExecuteMethod() { } + + internal void HandledBeforeExecuteMethod() => OnBeforeExecuteMethod(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Task CallNativeMethod(Func nativeMethod) => OnNativeMethodCalled(nativeMethod); + } } \ No newline at end of file diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index ea4671e3..623f33fa 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -1,630 +1,630 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using WebViewControl; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using WebViewControl; using Xilium.CefGlue; - -namespace ReactViewControl { - - internal partial class ReactViewRender : IChildViewHost, IDisposable { - -#if DEBUG - private static int counter; - private int id = counter++; -#endif - - private object SyncRoot { get; } = new object(); - - private const string CustomResourceBaseUrl = "resource"; - - private static Assembly ResourcesAssembly { get; } = typeof(ReactViewResources.Resources).Assembly; - - private Dictionary Frames { get; } = new Dictionary(); - private Dictionary> RecoverableFrames { get; } = new Dictionary>(); - - private ExtendedWebView WebView { get; } - private Assembly UserCallingAssembly { get; } - private LoaderModule Loader { get; } - private Func PluginsFactory { get; } - - private bool enableDebugMode; - private ResourceUrl defaultStyleSheet; - private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) - - public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null) { - UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; - - // must useSharedDomain for the local storage to be shared - WebView = new ExtendedWebView(useSharedDomain: true) { - DisableBuiltinContextMenus = true, - IsSecurityDisabled = true, - IgnoreMissingResources = false, - IsHistoryDisabled = true - }; - - NativeAPI.Initialize(this); - Loader = new LoaderModule(this); - - DefaultStyleSheet = defaultStyleSheet; - PluginsFactory = initializePlugins; - EnableDebugMode = enableDebugMode; - DevServerUri = devServerUri; - - GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame - - WebView.Disposed += Dispose; - WebView.BeforeNavigate += OnWebViewBeforeNavigate; - WebView.BeforeResourceLoad += OnWebViewBeforeResourceLoad; - WebView.LoadFailed += OnWebViewLoadFailed; - WebView.FilesDragging += fileNames => FilesDragging?.Invoke(fileNames); - WebView.TextDragging += textContent => TextDragging?.Invoke(textContent); - WebView.KeyPressed += OnWebViewKeyPressed; - - ExtraInitialize(); - - var urlParams = new string[] { - new ResourceUrl(ResourcesAssembly).ToString(), - enableDebugMode ? "true" : "false", - ExecutionEngine.ModulesObjectName, - NativeAPI.NativeObjectName, - ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl - }; - - WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams))); - - if (preloadWebView) { - PreloadWebView(); - } - - EditCommands = new EditCommands(WebView); - } - - partial void ExtraInitialize(); - - partial void PreloadWebView(); - - public ReactView Host { get; set; } - - /// - /// True when hot reload is enabled. - /// - public bool IsHotReloadEnabled => DevServerUri != null; - - public bool IsDisposing => WebView.IsDisposing; - - /// - /// True when the main component has been rendered. - /// - public bool IsReady => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus == LoadStatus.Ready; - - /// - /// True when view component is loading or loaded. - /// - public bool IsMainComponentLoaded => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus >= LoadStatus.ComponentLoading; - - /// - /// Enables or disables debug mode. - /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details - /// when a resource fails to load. - /// - public bool EnableDebugMode { - get { return enableDebugMode; } - set { - enableDebugMode = value; - WebView.AllowDeveloperTools = enableDebugMode; - if (enableDebugMode) { - WebView.ResourceLoadFailed += Loader.ShowResourceLoadFailedMessage; - } else { - WebView.ResourceLoadFailed -= Loader.ShowResourceLoadFailedMessage; - } - } - } - - /// - /// Gets webpack dev server url. - /// - public Uri DevServerUri { get; } - - /// - /// Gets or sets the webview zoom percentage (1 = 100%) - /// - public double ZoomPercentage { - get { return WebView.ZoomPercentage; } - set { WebView.ZoomPercentage = value; } - } - - /// - /// Event fired when the component is rendered and ready for interaction. - /// - public event Action Ready; - - /// - /// Event fired when an async exception occurs (eg: executing javascript) - /// - public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { - add { WebView.UnhandledAsyncException += value; } - remove { WebView.UnhandledAsyncException -= value; } - } - - /// - /// Event fired when a resource fails to load. - /// - public event ResourceLoadFailedEventHandler ResourceLoadFailed { - add { WebView.ResourceLoadFailed += value; } - remove { WebView.ResourceLoadFailed -= value; } - } - - /// - /// Handle embedded resource requests. You can use this event to change the resource being loaded. - /// - public event ResourceRequestedEventHandler EmbeddedResourceRequested; - - /// - /// Handle external resource requests. - /// Call to handle the request in an async way. - /// - public event ResourceRequestedEventHandler ExternalResourceRequested; - - /// - /// Handle custom resource requests. Use this event to load the resource based on provided key. - /// - public event CustomResourceRequestedEventHandler CustomResourceRequested; - - /// - /// Handle drag of files. Use this event to get the full path of the files being dragged. - /// - internal event FilesDraggingEventHandler FilesDragging; - - /// - /// Handle drag of text. Use this event to get the text content being dragged. - /// - internal event TextDraggingEventHandler TextDragging; - - /// - /// Gets the edition commands. - /// - internal EditCommands EditCommands { get; } - - /// - /// Javascript context was destroyed, cleanup everything. - /// - /// - private void OnWebViewJavascriptContextReleased(string frameName) { - if (!WebView.IsMainFrame(frameName)) { - // ignore, its an iframe saying goodbye - return; - } - - lock (SyncRoot) { - var mainFrame = Frames[FrameInfo.MainViewFrameName]; - - Frames.Remove(mainFrame.Name); - RecoverableFrames.Clear(); - foreach (var keyValuePair in Frames) { - RecoverableFrames[keyValuePair.Key] = new WeakReference(keyValuePair.Value); - UnregisterNativeObject(keyValuePair.Value.Component, keyValuePair.Value); - } - - Frames.Clear(); - Frames.Add(mainFrame.Name, mainFrame); - var previousComponentReady = mainFrame.IsComponentReadyToLoad; - mainFrame.Reset(); - mainFrame.IsComponentReadyToLoad = previousComponentReady; - } - } - - public void Dispose() { - WebView.Dispose(); - } - - /// - /// Initialize the underlying webview if has been initialized yet. - /// - public void EnsureInitialized() { - if (!WebView.IsBrowserInitialized) { - PreloadWebView(); - } - } - - /// - /// Binds the specified component to the main frame. - /// - /// - public void BindComponent(IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - BindComponentToFrame(component, frame); - } - } - - /// - /// Load the specified component into the main frame. - /// - /// - public void LoadComponent(IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - frame.IsComponentReadyToLoad = true; - TryLoadComponent(frame); - } - } - - void IChildViewHost.LoadComponent(string frameName, IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - // component not bound yet? bind it - BindComponentToFrame(component, frame); - } - frame.IsComponentReadyToLoad = true; - TryLoadComponent(frame); - } - } - - /// - /// Loads the frame component. - /// - /// - private void TryLoadComponent(FrameInfo frame) { - if (frame.Component == null || frame.LoadStatus != LoadStatus.ViewInitialized || !frame.IsComponentReadyToLoad) { - return; - } - - frame.LoadStatus = LoadStatus.ComponentLoading; - - RegisterNativeObject(frame.Component, frame); - - Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); - if (isInputDisabled && frame.IsMain) { - Loader.DisableMouseInteractions(); - } - } - - /// - /// Gets or sets the url of the default stylesheet. - /// - public ResourceUrl DefaultStyleSheet { - get { return defaultStyleSheet; } - set { - if (IsMainComponentLoaded) { - Loader.LoadDefaultStyleSheet(value); - } - defaultStyleSheet = value; - } - } - - private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { - var invalidPlugins = plugins.Where(p => string.IsNullOrEmpty(p.MainJsSource) || string.IsNullOrEmpty(p.Name) || string.IsNullOrEmpty(p.NativeObjectName)); - if (invalidPlugins.Any()) { - var pluginName = invalidPlugins.First().Name + "|" + invalidPlugins.First().GetType().Name; - throw new ArgumentException($"Plugin '{pluginName}' is invalid"); - } - - if (frame.LoadStatus > LoadStatus.ViewInitialized) { - throw new InvalidOperationException($"Cannot add plugins after component has been loaded"); - } - - frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); - - foreach (var plugin in plugins) { - plugin.Bind(frame); - } - } - - /// - /// Retrieves the specified plugin module instance for the spcifies frame. - /// - /// Type of the plugin to retrieve. - /// - /// If the plugin hasn't been registered on the specified frame. - /// - public T WithPlugin(string frameName = FrameInfo.MainViewFrameName) { - if (!Frames.TryGetValue(frameName, out var frame)) { - throw new InvalidOperationException($"Frame {frameName} is not loaded"); - } - - return frame.GetPlugin(); - } - - /// - /// Opens the developer tools. - /// - public void ShowDeveloperTools() { - WebView.ShowDeveloperTools(); - } - - /// - /// Closes the developer tools. - /// - public void CloseDeveloperTools() { - WebView.CloseDeveloperTools(); - } - - /// - /// Add an handler for custom resources from the specified frame. - /// - /// - /// - public void AddCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - frame.CustomResourceRequestedHandler += handler; - } - } - - /// - /// Remve the handler for custom resources from the specified frame. - /// - /// - /// - public void RemoveCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { - // do not create if frame does not exist - if (Frames.TryGetValue(frameName, out var frame)) { - frame.CustomResourceRequestedHandler -= handler; - } - } - - /// - /// Gets the view loaded on the specified frame. If none it will create a view of the specified - /// instance and bind it to the frame. - /// - /// - /// - public T GetOrAddChildView(string frameName) where T : IViewModule, new() { - T component; - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - component = new T(); - BindComponentToFrame(component, frame); - } else { - component = (T)frame.Component; - } - } - - return component; - } - - /// - /// Adds the specified view instance and binds it to the frame. - /// - /// - /// True if the view was bound to the frame. False if the frame already as a component bound. - public bool AddChildView(IViewModule childView, string frameName) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - BindComponentToFrame(childView, frame); - return true; - } - return false; - } - } - - /// - /// Binds the coponent to the specified frame. - /// - /// - /// - private void BindComponentToFrame(IViewModule component, FrameInfo frame) { - frame.Component = component; - component.Bind(frame, this); - } - - /// - /// Handles webview url load request. - /// - /// - private void OnWebViewBeforeNavigate(Request request) { - if (request.IsMainFrame && !request.Url.OrdinalStartsWith($"{ResourceUrl.EmbeddedScheme}{Uri.SchemeDelimiter}")) { - UrlHelper.OpenInExternalBrowser(request.Url); - request.Cancel(); - } - } - - /// - /// Handles the webview load of resources - /// - /// - private void OnWebViewBeforeResourceLoad(ResourceHandler resourceHandler) { - var url = resourceHandler.Url; - var scheme = url.Substring(0, Math.Max(0, url.OrdinalIndexOf(Uri.SchemeDelimiter))); - - switch (scheme.ToLowerInvariant()) { - case ResourceUrl.CustomScheme: - HandleCustomResourceRequested(resourceHandler); - break; - - case ResourceUrl.EmbeddedScheme: - // webview already started BeginAsyncResponse - EmbeddedResourceRequested?.Invoke(resourceHandler); - break; - - case "http": - case "https": - ExternalResourceRequested?.Invoke(resourceHandler); - break; - } - } - - /// - /// Handles webview load errors. - /// - /// - /// - /// The iframe name, not to be confused with view frame name - private void OnWebViewLoadFailed(string url, int errorCode, string frameName) { - if (!WebView.IsMainFrame(frameName)) { - // ignore errors in iframes - return; - } - - throw new Exception($"Failed to load view (error: {errorCode})"); - } - - /// - /// Handles webview keypresses. - /// - /// - /// - private void OnWebViewKeyPressed(CefKeyEvent keyEvent, out bool handled) { - handled = isInputDisabled; - } - - private CustomResourceRequestedEventHandler[] GetCustomResourceHandlers(FrameInfo frame) { - var globalHandlers = CustomResourceRequested?.GetInvocationList().Cast() ?? Enumerable.Empty(); - var frameHandlers = frame.CustomResourceRequestedHandler?.GetInvocationList().Cast() ?? Enumerable.Empty(); - return globalHandlers.Concat(frameHandlers).ToArray(); - } - - /// - /// Handle custom resource request and forward it to the appropriate frame. - /// - /// - /// - private void HandleCustomResourceRequested(ResourceHandler resourceHandler) { - var url = resourceHandler.Url; - - if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Segments.Length > 1 && uri.Host.Equals(CustomResourceBaseUrl, StringComparison.InvariantCultureIgnoreCase)) { - var frameName = uri.Segments.ElementAt(1).TrimEnd(ResourceUrl.PathSeparator.ToCharArray()); - if (frameName != null && Frames.TryGetValue(frameName, out var frame)) { - var customResourceRequestedHandlers = GetCustomResourceHandlers(frame); - if (customResourceRequestedHandlers.Any()) { - resourceHandler.BeginAsyncResponse(() => { - // get resource key from the query params - var resourceKeyAndOptions = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(p => Uri.UnescapeDataString(p)); - var resourceKey = resourceKeyAndOptions.FirstOrDefault(); - var options = resourceKeyAndOptions.Skip(1).ToArray(); - - // get response from first handler that returns a stream - var response = customResourceRequestedHandlers.Select(h => h(resourceKey, options)).FirstOrDefault(r => r?.Content != null); - - if (response != null) { - var extension = (response.Extension ?? Path.GetExtension(resourceKey)).TrimStart('.'); - resourceHandler.RespondWith(response.Content, extension); - } else { - resourceHandler.RespondWith(MemoryStream.Null); - } - }); - } - } - } - } - - /// - /// Registers a .net object to be available on the js context. - /// - /// - /// - /// - private void RegisterNativeObject(IViewModule module, FrameInfo frame) { - var nativeObjectName = module.GetNativeObjectFullName(frame.Name); - WebView.RegisterJavascriptObject(nativeObjectName, module.CreateNativeObject(), interceptCall: CallNativeMethod); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Task CallNativeMethod(Func nativeMethod) { - if (Host != null) { - return Host.CallNativeMethod(nativeMethod); - } - return Task.FromResult(nativeMethod()); - } - - /// - /// Unregisters a .net object available on the js context. - /// - /// - /// - private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { - var nativeObjectName = module.GetNativeObjectFullName(frame.Name); - WebView.UnregisterJavascriptObject(nativeObjectName); - } - - /// - /// Converts an url to a full path url - /// - /// - /// + +namespace ReactViewControl { + + internal partial class ReactViewRender : IChildViewHost, IDisposable { + +#if DEBUG + private static int counter; + private int id = counter++; +#endif + + private object SyncRoot { get; } = new object(); + + private const string CustomResourceBaseUrl = "resource"; + + private static Assembly ResourcesAssembly { get; } = typeof(ReactViewResources.Resources).Assembly; + + private Dictionary Frames { get; } = new Dictionary(); + private Dictionary> RecoverableFrames { get; } = new Dictionary>(); + + private ExtendedWebView WebView { get; } + private Assembly UserCallingAssembly { get; } + private LoaderModule Loader { get; } + private Func PluginsFactory { get; } + + private bool enableDebugMode; + private ResourceUrl defaultStyleSheet; + private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) + + public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null) { + UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; + + // must useSharedDomain for the local storage to be shared + WebView = new ExtendedWebView(useSharedDomain: true) { + DisableBuiltinContextMenus = true, + IsSecurityDisabled = true, + IgnoreMissingResources = false, + IsHistoryDisabled = true + }; + + NativeAPI.Initialize(this); + Loader = new LoaderModule(this); + + DefaultStyleSheet = defaultStyleSheet; + PluginsFactory = initializePlugins; + EnableDebugMode = enableDebugMode; + DevServerUri = devServerUri; + + GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame + + WebView.Disposed += Dispose; + WebView.BeforeNavigate += OnWebViewBeforeNavigate; + WebView.BeforeResourceLoad += OnWebViewBeforeResourceLoad; + WebView.LoadFailed += OnWebViewLoadFailed; + WebView.FilesDragging += fileNames => FilesDragging?.Invoke(fileNames); + WebView.TextDragging += textContent => TextDragging?.Invoke(textContent); + WebView.KeyPressed += OnWebViewKeyPressed; + + ExtraInitialize(); + + var urlParams = new string[] { + new ResourceUrl(ResourcesAssembly).ToString(), + enableDebugMode ? "true" : "false", + ExecutionEngine.ModulesObjectName, + NativeAPI.NativeObjectName, + ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl + }; + + WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams))); + + if (preloadWebView) { + PreloadWebView(); + } + + EditCommands = new EditCommands(WebView); + } + + partial void ExtraInitialize(); + + partial void PreloadWebView(); + + public ReactView Host { get; set; } + + /// + /// True when hot reload is enabled. + /// + public bool IsHotReloadEnabled => DevServerUri != null; + + public bool IsDisposing => WebView.IsDisposing; + + /// + /// True when the main component has been rendered. + /// + public bool IsReady => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus == LoadStatus.Ready; + + /// + /// True when view component is loading or loaded. + /// + public bool IsMainComponentLoaded => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus >= LoadStatus.ComponentLoading; + + /// + /// Enables or disables debug mode. + /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details + /// when a resource fails to load. + /// + public bool EnableDebugMode { + get { return enableDebugMode; } + set { + enableDebugMode = value; + WebView.AllowDeveloperTools = enableDebugMode; + if (enableDebugMode) { + WebView.ResourceLoadFailed += Loader.ShowResourceLoadFailedMessage; + } else { + WebView.ResourceLoadFailed -= Loader.ShowResourceLoadFailedMessage; + } + } + } + + /// + /// Gets webpack dev server url. + /// + public Uri DevServerUri { get; } + + /// + /// Gets or sets the webview zoom percentage (1 = 100%) + /// + public double ZoomPercentage { + get { return WebView.ZoomPercentage; } + set { WebView.ZoomPercentage = value; } + } + + /// + /// Event fired when the component is rendered and ready for interaction. + /// + public event Action Ready; + + /// + /// Event fired when an async exception occurs (eg: executing javascript) + /// + public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { + add { WebView.UnhandledAsyncException += value; } + remove { WebView.UnhandledAsyncException -= value; } + } + + /// + /// Event fired when a resource fails to load. + /// + public event ResourceLoadFailedEventHandler ResourceLoadFailed { + add { WebView.ResourceLoadFailed += value; } + remove { WebView.ResourceLoadFailed -= value; } + } + + /// + /// Handle embedded resource requests. You can use this event to change the resource being loaded. + /// + public event ResourceRequestedEventHandler EmbeddedResourceRequested; + + /// + /// Handle external resource requests. + /// Call to handle the request in an async way. + /// + public event ResourceRequestedEventHandler ExternalResourceRequested; + + /// + /// Handle custom resource requests. Use this event to load the resource based on provided key. + /// + public event CustomResourceRequestedEventHandler CustomResourceRequested; + + /// + /// Handle drag of files. Use this event to get the full path of the files being dragged. + /// + internal event FilesDraggingEventHandler FilesDragging; + + /// + /// Handle drag of text. Use this event to get the text content being dragged. + /// + internal event TextDraggingEventHandler TextDragging; + + /// + /// Gets the edition commands. + /// + internal EditCommands EditCommands { get; } + + /// + /// Javascript context was destroyed, cleanup everything. + /// + /// + private void OnWebViewJavascriptContextReleased(string frameName) { + if (!WebView.IsMainFrame(frameName)) { + // ignore, its an iframe saying goodbye + return; + } + + lock (SyncRoot) { + var mainFrame = Frames[FrameInfo.MainViewFrameName]; + + Frames.Remove(mainFrame.Name); + RecoverableFrames.Clear(); + foreach (var keyValuePair in Frames) { + RecoverableFrames[keyValuePair.Key] = new WeakReference(keyValuePair.Value); + UnregisterNativeObject(keyValuePair.Value.Component, keyValuePair.Value); + } + + Frames.Clear(); + Frames.Add(mainFrame.Name, mainFrame); + var previousComponentReady = mainFrame.IsComponentReadyToLoad; + mainFrame.Reset(); + mainFrame.IsComponentReadyToLoad = previousComponentReady; + } + } + + public void Dispose() { + WebView.Dispose(); + } + + /// + /// Initialize the underlying webview if has been initialized yet. + /// + public void EnsureInitialized() { + if (!WebView.IsBrowserInitialized) { + PreloadWebView(); + } + } + + /// + /// Binds the specified component to the main frame. + /// + /// + public void BindComponent(IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + BindComponentToFrame(component, frame); + } + } + + /// + /// Load the specified component into the main frame. + /// + /// + public void LoadComponent(IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + frame.IsComponentReadyToLoad = true; + TryLoadComponent(frame); + } + } + + void IChildViewHost.LoadComponent(string frameName, IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + // component not bound yet? bind it + BindComponentToFrame(component, frame); + } + frame.IsComponentReadyToLoad = true; + TryLoadComponent(frame); + } + } + + /// + /// Loads the frame component. + /// + /// + private void TryLoadComponent(FrameInfo frame) { + if (frame.Component == null || frame.LoadStatus != LoadStatus.ViewInitialized || !frame.IsComponentReadyToLoad) { + return; + } + + frame.LoadStatus = LoadStatus.ComponentLoading; + + RegisterNativeObject(frame.Component, frame); + + Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); + if (isInputDisabled && frame.IsMain) { + Loader.DisableMouseInteractions(); + } + } + + /// + /// Gets or sets the url of the default stylesheet. + /// + public ResourceUrl DefaultStyleSheet { + get { return defaultStyleSheet; } + set { + if (IsMainComponentLoaded) { + Loader.LoadDefaultStyleSheet(value); + } + defaultStyleSheet = value; + } + } + + private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { + var invalidPlugins = plugins.Where(p => string.IsNullOrEmpty(p.MainJsSource) || string.IsNullOrEmpty(p.Name) || string.IsNullOrEmpty(p.NativeObjectName)); + if (invalidPlugins.Any()) { + var pluginName = invalidPlugins.First().Name + "|" + invalidPlugins.First().GetType().Name; + throw new ArgumentException($"Plugin '{pluginName}' is invalid"); + } + + if (frame.LoadStatus > LoadStatus.ViewInitialized) { + throw new InvalidOperationException($"Cannot add plugins after component has been loaded"); + } + + frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); + + foreach (var plugin in plugins) { + plugin.Bind(frame); + } + } + + /// + /// Retrieves the specified plugin module instance for the spcifies frame. + /// + /// Type of the plugin to retrieve. + /// + /// If the plugin hasn't been registered on the specified frame. + /// + public T WithPlugin(string frameName = FrameInfo.MainViewFrameName) { + if (!Frames.TryGetValue(frameName, out var frame)) { + throw new InvalidOperationException($"Frame {frameName} is not loaded"); + } + + return frame.GetPlugin(); + } + + /// + /// Opens the developer tools. + /// + public void ShowDeveloperTools() { + WebView.ShowDeveloperTools(); + } + + /// + /// Closes the developer tools. + /// + public void CloseDeveloperTools() { + WebView.CloseDeveloperTools(); + } + + /// + /// Add an handler for custom resources from the specified frame. + /// + /// + /// + public void AddCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + frame.CustomResourceRequestedHandler += handler; + } + } + + /// + /// Remve the handler for custom resources from the specified frame. + /// + /// + /// + public void RemoveCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { + // do not create if frame does not exist + if (Frames.TryGetValue(frameName, out var frame)) { + frame.CustomResourceRequestedHandler -= handler; + } + } + + /// + /// Gets the view loaded on the specified frame. If none it will create a view of the specified + /// instance and bind it to the frame. + /// + /// + /// + public T GetOrAddChildView(string frameName) where T : IViewModule, new() { + T component; + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + component = new T(); + BindComponentToFrame(component, frame); + } else { + component = (T)frame.Component; + } + } + + return component; + } + + /// + /// Adds the specified view instance and binds it to the frame. + /// + /// + /// True if the view was bound to the frame. False if the frame already as a component bound. + public bool AddChildView(IViewModule childView, string frameName) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + BindComponentToFrame(childView, frame); + return true; + } + return false; + } + } + + /// + /// Binds the component to the specified frame. + /// + /// + /// + private void BindComponentToFrame(IViewModule component, FrameInfo frame) { + frame.Component = component; + component.Bind(frame, this); + } + + /// + /// Handles webview url load request. + /// + /// + private void OnWebViewBeforeNavigate(Request request) { + if (request.IsMainFrame && !request.Url.OrdinalStartsWith($"{ResourceUrl.EmbeddedScheme}{Uri.SchemeDelimiter}")) { + UrlHelper.OpenInExternalBrowser(request.Url); + request.Cancel(); + } + } + + /// + /// Handles the webview load of resources + /// + /// + private void OnWebViewBeforeResourceLoad(ResourceHandler resourceHandler) { + var url = resourceHandler.Url; + var scheme = url.Substring(0, Math.Max(0, url.OrdinalIndexOf(Uri.SchemeDelimiter))); + + switch (scheme.ToLowerInvariant()) { + case ResourceUrl.CustomScheme: + HandleCustomResourceRequested(resourceHandler); + break; + + case ResourceUrl.EmbeddedScheme: + // webview already started BeginAsyncResponse + EmbeddedResourceRequested?.Invoke(resourceHandler); + break; + + case "http": + case "https": + ExternalResourceRequested?.Invoke(resourceHandler); + break; + } + } + + /// + /// Handles webview load errors. + /// + /// + /// + /// The iframe name, not to be confused with view frame name + private void OnWebViewLoadFailed(string url, int errorCode, string frameName) { + if (!WebView.IsMainFrame(frameName)) { + // ignore errors in iframes + return; + } + + throw new Exception($"Failed to load view (error: {errorCode})"); + } + + /// + /// Handles webview keypresses. + /// + /// + /// + private void OnWebViewKeyPressed(CefKeyEvent keyEvent, out bool handled) { + handled = isInputDisabled; + } + + private CustomResourceRequestedEventHandler[] GetCustomResourceHandlers(FrameInfo frame) { + var globalHandlers = CustomResourceRequested?.GetInvocationList().Cast() ?? Enumerable.Empty(); + var frameHandlers = frame.CustomResourceRequestedHandler?.GetInvocationList().Cast() ?? Enumerable.Empty(); + return globalHandlers.Concat(frameHandlers).ToArray(); + } + + /// + /// Handle custom resource request and forward it to the appropriate frame. + /// + /// + /// + private void HandleCustomResourceRequested(ResourceHandler resourceHandler) { + var url = resourceHandler.Url; + + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Segments.Length > 1 && uri.Host.Equals(CustomResourceBaseUrl, StringComparison.InvariantCultureIgnoreCase)) { + var frameName = uri.Segments.ElementAt(1).TrimEnd(ResourceUrl.PathSeparator.ToCharArray()); + if (frameName != null && Frames.TryGetValue(frameName, out var frame)) { + var customResourceRequestedHandlers = GetCustomResourceHandlers(frame); + if (customResourceRequestedHandlers.Any()) { + resourceHandler.BeginAsyncResponse(() => { + // get resource key from the query params + var resourceKeyAndOptions = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(p => Uri.UnescapeDataString(p)); + var resourceKey = resourceKeyAndOptions.FirstOrDefault(); + var options = resourceKeyAndOptions.Skip(1).ToArray(); + + // get response from first handler that returns a stream + var response = customResourceRequestedHandlers.Select(h => h(resourceKey, options)).FirstOrDefault(r => r?.Content != null); + + if (response != null) { + var extension = (response.Extension ?? Path.GetExtension(resourceKey)).TrimStart('.'); + resourceHandler.RespondWith(response.Content, extension); + } else { + resourceHandler.RespondWith(MemoryStream.Null); + } + }); + } + } + } + } + + /// + /// Registers a .net object to be available on the js context. + /// + /// + /// + /// + private void RegisterNativeObject(IViewModule module, FrameInfo frame) { + var nativeObjectName = module.GetNativeObjectFullName(frame.Name); + WebView.RegisterJavascriptObject(nativeObjectName, module.CreateNativeObject(), interceptCall: CallNativeMethod); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Task CallNativeMethod(Func nativeMethod) { + if (Host != null) { + return Host.CallNativeMethod(nativeMethod); + } + return Task.FromResult(nativeMethod()); + } + + /// + /// Unregisters a .net object available on the js context. + /// + /// + /// + private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { + var nativeObjectName = module.GetNativeObjectFullName(frame.Name); + WebView.UnregisterJavascriptObject(nativeObjectName); + } + + /// + /// Converts an url to a full path url + /// + /// + /// private string ToFullUrl(string url) { - if (url.OrdinalContains(Uri.SchemeDelimiter)) { - return url; - } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { - if (IsHotReloadEnabled) { - return new Uri(DevServerUri, url).ToString(); - } else { - return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); - } - } else { - return new ResourceUrl(UserCallingAssembly, url).ToString(); - } - } - - /// - /// Normalizes the url path separators - /// - /// - /// - private static string NormalizeUrl(string url) { - return url.Replace("\\", ResourceUrl.PathSeparator); - } - - /// - /// Disables or enables keyboard and mouse interactions with the browser - /// - /// - private void DisableInputInteractions(bool disable) { - if (isInputDisabled == disable) { - return; - } - lock (SyncRoot) { - if (isInputDisabled == disable) { - return; - } - isInputDisabled = disable; - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - if (frame.LoadStatus >= LoadStatus.ComponentLoading) { - if (disable) { - Loader.DisableMouseInteractions(); - } else { - Loader.EnableMouseInteractions(); - } - } - } - } - - private FrameInfo GetOrCreateFrame(string frameName) { - if (Frames.TryGetValue(frameName, out var frame)) { - return frame; - } - - if (RecoverableFrames.TryGetValue(frameName, out var weakReferenceFrame)) { - RecoverableFrames.Remove(frameName); - if (weakReferenceFrame.TryGetTarget(out var recoverableFrame)) { - Frames[frameName] = recoverableFrame; - return recoverableFrame; - } - } - - var newFrame = new FrameInfo(frameName); - Frames[frameName] = newFrame; - AddPlugins(PluginsFactory(), newFrame); - - return newFrame; - } - - private static MethodBase GetUserCallingMethod(bool captureFilenames = false) { - var currentAssembly = typeof(ReactView).Assembly; - var callstack = new StackTrace(captureFilenames).GetFrames().Select(f => f.GetMethod()).Where(m => m.ReflectedType.Assembly != currentAssembly); - var userMethod = callstack.First(m => !IsFrameworkAssemblyName(m.ReflectedType.Assembly.GetName().Name)); - if (userMethod == null) { - throw new InvalidOperationException("Unable to find calling method"); - } - return userMethod; - } - } + if (url.OrdinalContains(Uri.SchemeDelimiter)) { + return url; + } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { + if (IsHotReloadEnabled) { + return new Uri(DevServerUri, url).ToString(); + } else { + return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); + } + } else { + return new ResourceUrl(UserCallingAssembly, url).ToString(); + } + } + + /// + /// Normalizes the url path separators + /// + /// + /// + private static string NormalizeUrl(string url) { + return url.Replace("\\", ResourceUrl.PathSeparator); + } + + /// + /// Disables or enables keyboard and mouse interactions with the browser + /// + /// + private void DisableInputInteractions(bool disable) { + if (isInputDisabled == disable) { + return; + } + lock (SyncRoot) { + if (isInputDisabled == disable) { + return; + } + isInputDisabled = disable; + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + if (frame.LoadStatus >= LoadStatus.ComponentLoading) { + if (disable) { + Loader.DisableMouseInteractions(); + } else { + Loader.EnableMouseInteractions(); + } + } + } + } + + private FrameInfo GetOrCreateFrame(string frameName) { + if (Frames.TryGetValue(frameName, out var frame)) { + return frame; + } + + if (RecoverableFrames.TryGetValue(frameName, out var weakReferenceFrame)) { + RecoverableFrames.Remove(frameName); + if (weakReferenceFrame.TryGetTarget(out var recoverableFrame)) { + Frames[frameName] = recoverableFrame; + return recoverableFrame; + } + } + + var newFrame = new FrameInfo(frameName); + Frames[frameName] = newFrame; + AddPlugins(PluginsFactory(), newFrame); + + return newFrame; + } + + private static MethodBase GetUserCallingMethod(bool captureFilenames = false) { + var currentAssembly = typeof(ReactView).Assembly; + var callstack = new StackTrace(captureFilenames).GetFrames().Select(f => f.GetMethod()).Where(m => m.ReflectedType.Assembly != currentAssembly); + var userMethod = callstack.First(m => !IsFrameworkAssemblyName(m.ReflectedType.Assembly.GetName().Name)); + if (userMethod == null) { + throw new InvalidOperationException("Unable to find calling method"); + } + return userMethod; + } + } } \ No newline at end of file diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 55d571e1..4474fdb4 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -1,135 +1,101 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using WebViewControl; - -namespace ReactViewControl { - - public abstract class ViewModuleContainer : IViewModule { - - private const string JsEntryFileExtension = ".js.entry"; - private const string CssEntryFileExtension = ".css.entry"; - - private IFrame frame; - private IChildViewHost childViewHost; - - public ViewModuleContainer() { - DependencyJsSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(JsEntryFileExtension)); - CssSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(CssEntryFileExtension)); - - frame = new FrameInfo("dummy"); - } - - private Lazy DependencyJsSourcesCache { get; } - private Lazy CssSourcesCache { get; } - - protected virtual string MainJsSource => null; - protected virtual string NativeObjectName => null; - protected virtual string ModuleName => null; - protected virtual string Source => null; - - protected virtual object CreateNativeObject() => null; - - protected virtual string[] Events => new string[0]; - - protected virtual string[] DependencyJsSources => new string[0]; - - protected virtual string[] CssSources => new string[0]; - - protected virtual KeyValuePair[] PropertiesValues => new KeyValuePair[0]; - - string IViewModule.MainJsSource => MainJsSource; - - string IViewModule.NativeObjectName => NativeObjectName; - - string IViewModule.Name => ModuleName; - - string IViewModule.Source => Source; - - object IViewModule.CreateNativeObject() => CreateNativeObject(); - - string[] IViewModule.Events => Events; - - string[] IViewModule.DependencyJsSources => DependencyJsSourcesCache.Value; - - string[] IViewModule.CssSources => CssSourcesCache.Value; - - KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; - - void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { - frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; - frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); - this.frame = frame; - this.childViewHost = childViewHost; - } - - // ease access in generated code - protected IExecutionEngine ExecutionEngine { - get { - var engine = frame.ExecutionEngine; - if (engine == null) { - throw new InvalidOperationException("View module must be bound to an execution engine"); - } - return engine; - } - } - - private string[] GetDependenciesFromEntriesFile(string extension) { - var entriesFilePath = Path.Combine(Path.GetDirectoryName(MainJsSource), Path.GetFileNameWithoutExtension(MainJsSource) + extension); - var resource = entriesFilePath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - - using (var stream = GetResourceStream(resource)) { - if (stream != null) { - using (var reader = new StreamReader(stream)) { - var allEntries = reader.ReadToEnd(); - if (allEntries != null && allEntries != string.Empty) { - return allEntries.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); - } - } - } - } - return new string[0]; - } - - private Stream GetResourceStream(string[] resource) { - var isHotReloadEnabled = childViewHost == null ? false : childViewHost.IsHotReloadEnabled; - if (isHotReloadEnabled) { - return File.OpenRead(Path.Combine(Path.GetDirectoryName(Source), resource.Last())); - } - - return ResourcesManager.TryGetResourceWithFullPath(resource.First(), resource); - } - - public event CustomResourceRequestedEventHandler CustomResourceRequested { - add => frame.CustomResourceRequestedHandler += value; - remove => frame.CustomResourceRequestedHandler -= value; - } - - public T WithPlugin() { - return frame.GetPlugin(); - } - - public void Load() { - childViewHost?.LoadComponent(frame.Name, this); - } - - public T GetOrAddChildView(string frameName) where T : IViewModule, new() { - if (childViewHost == null) { - return default(T); - } - return childViewHost.GetOrAddChildView(FormatChildViewName(frameName)); - } - - public bool AddChildView(IViewModule childView, string frameName) { - if (childViewHost == null) { - return false; - } - return childViewHost.AddChildView(childView, FormatChildViewName(frameName)); - } - - public ReactView Host => childViewHost?.Host; - - private string FormatChildViewName(string name) => frame.Name + (string.IsNullOrEmpty(frame.Name) ? "" : ".") + name; - } -} +using System; +using System.Collections.Generic; + +namespace ReactViewControl { + + public abstract class ViewModuleContainer : IViewModule { + + private const string JsEntryFileExtension = ".js.entry"; + private const string CssEntryFileExtension = ".css.entry"; + + private IFrame frame; + private IChildViewHost childViewHost; + private IModuleDependenciesProvider dependenciesProvider; + + public ViewModuleContainer() { + frame = new FrameInfo("dummy"); + } + + protected virtual IModuleDependenciesProvider DependenciesProvider { + get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } + } + + protected virtual string MainJsSource => null; + protected virtual string NativeObjectName => null; + protected virtual string ModuleName => null; + protected virtual string Source => null; + + protected virtual object CreateNativeObject() => null; + + protected virtual string[] Events => new string[0]; + + protected virtual KeyValuePair[] PropertiesValues => new KeyValuePair[0]; + + string IViewModule.MainJsSource => MainJsSource; + + string IViewModule.NativeObjectName => NativeObjectName; + + string IViewModule.Name => ModuleName; + + string IViewModule.Source => Source; + + object IViewModule.CreateNativeObject() => CreateNativeObject(); + + string[] IViewModule.Events => Events; + + string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(JsEntryFileExtension); + + string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(CssEntryFileExtension); + + KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; + + void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { + frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; + frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); + this.frame = frame; + this.childViewHost = childViewHost; + } + + // ease access in generated code + protected IExecutionEngine ExecutionEngine { + get { + var engine = frame.ExecutionEngine; + if (engine == null) { + throw new InvalidOperationException("View module must be bound to an execution engine"); + } + return engine; + } + } + + public event CustomResourceRequestedEventHandler CustomResourceRequested { + add => frame.CustomResourceRequestedHandler += value; + remove => frame.CustomResourceRequestedHandler -= value; + } + + public T WithPlugin() { + return frame.GetPlugin(); + } + + public void Load() { + childViewHost?.LoadComponent(frame.Name, this); + } + + public T GetOrAddChildView(string frameName) where T : IViewModule, new() { + if (childViewHost == null) { + return default(T); + } + return childViewHost.GetOrAddChildView(FormatChildViewName(frameName)); + } + + public bool AddChildView(IViewModule childView, string frameName) { + if (childViewHost == null) { + return false; + } + return childViewHost.AddChildView(childView, FormatChildViewName(frameName)); + } + + public ReactView Host => childViewHost?.Host; + + private string FormatChildViewName(string name) => frame.Name + (string.IsNullOrEmpty(frame.Name) ? "" : ".") + name; + } +} diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index a772f7f5..ca6ad534 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -1,22 +1,36 @@ -using ReactViewControl; - -namespace Sample.Avalonia { - - public abstract class ExtendedReactView : ReactView { - - protected override ReactViewFactory Factory => new ExtendedReactViewFactory(); - - public ExtendedReactView(IViewModule mainModule) : base(mainModule) { - Settings.ThemeChanged += OnStylePreferenceChanged; - } - - protected override void InnerDispose() { - base.InnerDispose(); - Settings.ThemeChanged -= OnStylePreferenceChanged; - } - - private void OnStylePreferenceChanged() { - RefreshDefaultStyleSheet(); - } - } -} +using System; +using ReactViewControl; + +namespace Sample.Avalonia { + + public abstract class ExtendedReactView : ReactView { + + protected override ReactViewFactory Factory => new ExtendedReactViewFactory(); + + public ExtendedReactView(IViewModule mainModule) : base(mainModule) { + Settings.ThemeChanged += OnStylePreferenceChanged; + EmbeddedResourceRequested += OnEmbeddedResourceRequested; + } + + protected override void InnerDispose() { + base.InnerDispose(); + Settings.ThemeChanged -= OnStylePreferenceChanged; + } + + private void OnStylePreferenceChanged() { + RefreshDefaultStyleSheet(); + } + + private void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { + var resourceUrl = resourceHandler.Url; + + if (resourceUrl.Contains("ReactViewResources")) { + return; + } + + resourceUrl = new Uri(resourceUrl).PathAndQuery; + var devServerHost = new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); + resourceHandler.Redirect(new Uri(devServerHost, resourceUrl).ToString()); + } + } +} From 267fe0c66b3e8d46beab9c2eed825455539c0365 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Fri, 27 Oct 2023 11:55:28 +0100 Subject: [PATCH 02/33] attempt to set modules provider --- ReactViewControl/IViewModule.cs | 4 +- ReactViewControl/ViewModuleContainer.cs | 3 +- Sample.Avalonia/ExtendedReactView.cs | 1 + Sample.Avalonia/ExtendedReactViewFactory.cs | 56 +++++++------- .../WebpackDevDependenciesProvider.cs | 76 +++++++++++++++++++ 5 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 Sample.Avalonia/WebpackDevDependenciesProvider.cs diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index ba2ac6d7..058c5a5a 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -4,7 +4,9 @@ namespace ReactViewControl { public interface IViewModule { - string MainJsSource { get; } + public string MainJsSource { get; } + + IModuleDependenciesProvider DependenciesProvider{ get; set; } string[] DependencyJsSources { get; } diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 4474fdb4..7e62f329 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -16,8 +16,9 @@ public ViewModuleContainer() { frame = new FrameInfo("dummy"); } - protected virtual IModuleDependenciesProvider DependenciesProvider { + public virtual IModuleDependenciesProvider DependenciesProvider { get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } + set { dependenciesProvider = value; } } protected virtual string MainJsSource => null; diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index ca6ad534..fc5ed23b 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -10,6 +10,7 @@ public abstract class ExtendedReactView : ReactView { public ExtendedReactView(IViewModule mainModule) : base(mainModule) { Settings.ThemeChanged += OnStylePreferenceChanged; EmbeddedResourceRequested += OnEmbeddedResourceRequested; + mainModule.DependenciesProvider = new WebPackDependenciesProvider(Factory.DevServerURI); } protected override void InnerDispose() { diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 253b458c..ae723bfb 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -1,28 +1,28 @@ -using System; -using ReactViewControl; -using WebViewControl; - -namespace Sample.Avalonia { - - internal class ExtendedReactViewFactory : ReactViewFactory { - - public override ResourceUrl DefaultStyleSheet => - new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); - - public override IViewModule[] InitializePlugins() { - return new[]{ - new ViewPlugin() - }; - } - - public override bool ShowDeveloperTools => false; - - public override bool EnableViewPreload => true; - -#if DEBUG - public override bool EnableDebugMode => true; - - public override Uri DevServerURI => null; -#endif - } -} +using System; +using ReactViewControl; +using WebViewControl; + +namespace Sample.Avalonia { + + internal class ExtendedReactViewFactory : ReactViewFactory { + + public override ResourceUrl DefaultStyleSheet => + new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); + + public override IViewModule[] InitializePlugins() { + return new[]{ + new ViewPlugin() + }; + } + + public override bool ShowDeveloperTools => false; + + public override bool EnableViewPreload => true; + +#if DEBUG + public override bool EnableDebugMode => true; + + public override Uri DevServerURI => new Uri("http://localhost:8080/"); +#endif + } +} diff --git a/Sample.Avalonia/WebpackDevDependenciesProvider.cs b/Sample.Avalonia/WebpackDevDependenciesProvider.cs new file mode 100644 index 00000000..d8fd077b --- /dev/null +++ b/Sample.Avalonia/WebpackDevDependenciesProvider.cs @@ -0,0 +1,76 @@ +using ReactViewControl; + +namespace Sample.Avalonia; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +//using Newtonsoft.Json; + +public class WebPackDependenciesProvider : IModuleDependenciesProvider { + + private Dictionary> dependencies; + private readonly Uri uri; + private readonly string basePath; + private const string ManifestPath = "manifest.json"; + + private DateTime lastRefresh; + + public WebPackDependenciesProvider(Uri uri) { + this.uri = uri; + basePath = GetBaseSegmentFromUri(); + RefreshDependencies(); + } + + public string GetBaseSegmentFromUri() { + return "/" + uri.Segments.Last(); + } + + string[] IModuleDependenciesProvider.GetCssDependencies(string filename) { + var entriesFilePath = Path.GetFileNameWithoutExtension(filename); + + if (!dependencies.ContainsKey(entriesFilePath)) { + return new string[0]; + } + + return dependencies[entriesFilePath] + .Where(dependency => dependency.Contains(".css")) + .Select(dependency => basePath + dependency) + .ToArray(); + } + + string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { + var entriesFilePath = Path.GetFileNameWithoutExtension(filename); + + if (!dependencies.ContainsKey(entriesFilePath)) { + return new string[0]; + } + + return dependencies[entriesFilePath] + .FindAll(dependency => dependency.Contains(".js")) + .Select(dependency => basePath + dependency) + .Reverse().Skip(1).Reverse().ToArray() // remove self reference + .ToArray(); + } + + public void RefreshDependencies() { + var shouldRefresh = true; + + if (lastRefresh != null) { + var timeSpan = DateTime.Now.Subtract(lastRefresh); + if (timeSpan.TotalSeconds < 10) { + shouldRefresh = false; + } + } + + if (shouldRefresh) { + lastRefresh = DateTime.Now; + using (var wc = new WebClient()) { + var json = wc.DownloadString(new Uri(uri, ManifestPath)); + // dependencies = JsonConvert.DeserializeObject>>(json); + } + } + } +} From e6b5e063dd67f23e82931adda5ad5afe3802e099 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 11:56:37 +0000 Subject: [PATCH 03/33] working, refactor is needed --- Directory.Packages.props | 6 +- ReactViewControl/ReactView.cs | 2 +- ReactViewControl/ReactViewFactory.cs | 83 +++++---- ReactViewControl/ReactViewRender.cs | 15 +- ReactViewControl/ViewModuleContainer.cs | 4 +- Sample.Avalonia/ExtendedReactView.cs | 2 +- Sample.Avalonia/ExtendedReactViewFactory.cs | 14 +- Sample.Avalonia/MainView/MainView.tsx | 4 +- Sample.Avalonia/TaskListView/TaskListView.tsx | 3 +- .../WebpackDevDependenciesProvider.cs | 30 ++-- Sample.Avalonia/rundevserver.sh | 12 ++ ViewGeneratorCore/tools/package-lock.json | 170 +++++++++++++++++- 12 files changed, 279 insertions(+), 66 deletions(-) create mode 100644 Sample.Avalonia/rundevserver.sh diff --git a/Directory.Packages.props b/Directory.Packages.props index f595cb3f..0217ee41 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,11 +22,11 @@ - + - + @@ -35,6 +35,6 @@ - + diff --git a/ReactViewControl/ReactView.cs b/ReactViewControl/ReactView.cs index beaedadf..cedce918 100644 --- a/ReactViewControl/ReactView.cs +++ b/ReactViewControl/ReactView.cs @@ -19,7 +19,7 @@ public abstract partial class ReactView : IDisposable { private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { ReactViewRender InnerCreateView() { - var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.DevServerURI); + var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.DevServerURI, factory.ModuleDependenciesProvider); if (factory.ShowDeveloperTools) { view.ShowDeveloperTools(); } diff --git a/ReactViewControl/ReactViewFactory.cs b/ReactViewControl/ReactViewFactory.cs index 2e488587..2560aa79 100644 --- a/ReactViewControl/ReactViewFactory.cs +++ b/ReactViewControl/ReactViewFactory.cs @@ -1,39 +1,44 @@ -using System; -using WebViewControl; - -namespace ReactViewControl { - - public class ReactViewFactory { - - /// - /// The default stylesheet. - /// - public virtual ResourceUrl DefaultStyleSheet => null; - - /// - /// Place plugins initialization here and return the plugins modules instances. - /// - /// - public virtual IViewModule[] InitializePlugins() => new IViewModule[0]; - - /// - /// Shows developers tools when the control is instantiated. - /// - public virtual bool ShowDeveloperTools => false; - - /// - /// Developer tools become available pressing F12. - /// - public virtual bool EnableDebugMode => false; - - /// - /// The view is cached and preloaded. First render occurs earlier. - /// - public virtual bool EnableViewPreload => true; - - /// - /// Webpack dev server url. Setting this value will enable hot reload. eg: new Uri("http://localhost:8080") - /// - public virtual Uri DevServerURI => null; - } -} +using System; +using WebViewControl; + +namespace ReactViewControl { + + public class ReactViewFactory { + + /// + /// The default stylesheet. + /// + public virtual ResourceUrl DefaultStyleSheet => null; + + /// + /// Place plugins initialization here and return the plugins modules instances. + /// + /// + public virtual IViewModule[] InitializePlugins() => new IViewModule[0]; + + /// + /// Shows developers tools when the control is instantiated. + /// + public virtual bool ShowDeveloperTools => false; + + /// + /// Developer tools become available pressing F12. + /// + public virtual bool EnableDebugMode => false; + + /// + /// The view is cached and preloaded. First render occurs earlier. + /// + public virtual bool EnableViewPreload => true; + + /// + /// Webpack dev server url. Setting this value will enable hot reload. eg: new Uri("http://localhost:8080") + /// + public virtual Uri DevServerURI => null; + + /// + /// Module dependencies provider. + /// + public virtual IModuleDependenciesProvider ModuleDependenciesProvider => null; + } +} diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 623f33fa..8cc8fccc 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -36,7 +36,7 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable { private ResourceUrl defaultStyleSheet; private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) - public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null) { + public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null, IModuleDependenciesProvider moduleDependenciesProvider = null) { UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; // must useSharedDomain for the local storage to be shared @@ -54,6 +54,7 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func initia PluginsFactory = initializePlugins; EnableDebugMode = enableDebugMode; DevServerUri = devServerUri; + ModuleDependenciesProvider = moduleDependenciesProvider; GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame @@ -130,6 +131,9 @@ public bool EnableDebugMode { /// public Uri DevServerUri { get; } + public IModuleDependenciesProvider ModuleDependenciesProvider { get; } + + /// /// Gets or sets the webview zoom percentage (1 = 100%) /// @@ -279,11 +283,15 @@ private void TryLoadComponent(FrameInfo frame) { RegisterNativeObject(frame.Component, frame); + if (ModuleDependenciesProvider != null) { + frame.Component.DependenciesProvider = ModuleDependenciesProvider; + } + Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); if (isInputDisabled && frame.IsMain) { Loader.DisableMouseInteractions(); } - } + } /// /// Gets or sets the url of the default stylesheet. @@ -551,11 +559,12 @@ private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { /// /// private string ToFullUrl(string url) { + // TODO: BUG AQUI if (url.OrdinalContains(Uri.SchemeDelimiter)) { return url; } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { if (IsHotReloadEnabled) { - return new Uri(DevServerUri, url).ToString(); + return new Uri(DevServerUri, url).ToString(); } else { return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); } diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 7e62f329..b5899afd 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -44,9 +44,9 @@ public virtual IModuleDependenciesProvider DependenciesProvider { string[] IViewModule.Events => Events; - string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(JsEntryFileExtension); + string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(MainJsSource); - string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(CssEntryFileExtension); + string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(MainJsSource); KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index fc5ed23b..e85094a2 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -10,7 +10,7 @@ public abstract class ExtendedReactView : ReactView { public ExtendedReactView(IViewModule mainModule) : base(mainModule) { Settings.ThemeChanged += OnStylePreferenceChanged; EmbeddedResourceRequested += OnEmbeddedResourceRequested; - mainModule.DependenciesProvider = new WebPackDependenciesProvider(Factory.DevServerURI); + mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; } protected override void InnerDispose() { diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index ae723bfb..27d7f686 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -5,13 +5,20 @@ namespace Sample.Avalonia { internal class ExtendedReactViewFactory : ReactViewFactory { + private static WebPackDependenciesProvider provider = new WebPackDependenciesProvider(new Uri("http://localhost:8080/Sample.Avalonia/")); public override ResourceUrl DefaultStyleSheet => new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); public override IViewModule[] InitializePlugins() { + var viewPlugin = new ViewPlugin(); +#if DEBUG + if (DevServerURI != null) { + viewPlugin.DependenciesProvider = ModuleDependenciesProvider; + } +#endif return new[]{ - new ViewPlugin() + viewPlugin }; } @@ -22,7 +29,10 @@ public override IViewModule[] InitializePlugins() { #if DEBUG public override bool EnableDebugMode => true; - public override Uri DevServerURI => new Uri("http://localhost:8080/"); + public override Uri DevServerURI => new Uri("http://localhost:8080/Sample.Avalonia/"); + + public override IModuleDependenciesProvider ModuleDependenciesProvider => + provider; #endif } } diff --git a/Sample.Avalonia/MainView/MainView.tsx b/Sample.Avalonia/MainView/MainView.tsx index 2250df54..bd868cab 100644 --- a/Sample.Avalonia/MainView/MainView.tsx +++ b/Sample.Avalonia/MainView/MainView.tsx @@ -5,7 +5,7 @@ import { IPluginsContext } from "PluginsProvider"; import "./MainView.scss"; // import a stylesheet import TaskListView from "./../TaskListView/TaskListView"; // import another component import * as Styles from "./MainView.scss"; // import variables from SASS -import * as BackgroundImage from "./Tasks.png"; // import images +//import * as BackgroundImage from "./Tasks.png"; // import images export interface ITaskCreationDetails { text: string; @@ -78,7 +78,7 @@ export default class MainView extends React.Component
{this.props.task.text}
+
Added by: @@ -98,7 +99,7 @@ export default class TaskListView extends React.Component +
xau {this.renderItems()}
); diff --git a/Sample.Avalonia/WebpackDevDependenciesProvider.cs b/Sample.Avalonia/WebpackDevDependenciesProvider.cs index d8fd077b..7da81ffc 100644 --- a/Sample.Avalonia/WebpackDevDependenciesProvider.cs +++ b/Sample.Avalonia/WebpackDevDependenciesProvider.cs @@ -1,3 +1,6 @@ +using System.Net.Http; +using System.Reflection; +using System.Text.Json; using ReactViewControl; namespace Sample.Avalonia; @@ -25,7 +28,9 @@ public WebPackDependenciesProvider(Uri uri) { } public string GetBaseSegmentFromUri() { - return "/" + uri.Segments.Last(); + // TODO: fixme + return uri.ToString(); + //return "/" + uri.Segments.Last(); } string[] IModuleDependenciesProvider.GetCssDependencies(string filename) { @@ -35,6 +40,7 @@ string[] IModuleDependenciesProvider.GetCssDependencies(string filename) { return new string[0]; } + RefreshDependencies(); return dependencies[entriesFilePath] .Where(dependency => dependency.Contains(".css")) .Select(dependency => basePath + dependency) @@ -48,8 +54,9 @@ string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { return new string[0]; } + RefreshDependencies(); return dependencies[entriesFilePath] - .FindAll(dependency => dependency.Contains(".js")) + .Where(dependency => dependency.Contains(".js") && !dependency.Contains("ViewsRuntime")) .Select(dependency => basePath + dependency) .Reverse().Skip(1).Reverse().ToArray() // remove self reference .ToArray(); @@ -58,19 +65,20 @@ string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { public void RefreshDependencies() { var shouldRefresh = true; - if (lastRefresh != null) { - var timeSpan = DateTime.Now.Subtract(lastRefresh); - if (timeSpan.TotalSeconds < 10) { - shouldRefresh = false; - } + var timeSpan = DateTime.Now.Subtract(lastRefresh); + if (timeSpan.TotalSeconds < 5) { + shouldRefresh = false; } if (shouldRefresh) { + using var httpClient = new HttpClient(); + var assembly = typeof(Program).Assembly.GetName().Name; + // var json = httpClient.GetStringAsync(new Uri(uri, $"{assembly}/{ManifestPath}")); + var json = httpClient.GetStringAsync(new Uri(uri, ManifestPath)); + json.Wait(); + + dependencies = JsonSerializer.Deserialize>>(json.Result); lastRefresh = DateTime.Now; - using (var wc = new WebClient()) { - var json = wc.DownloadString(new Uri(uri, ManifestPath)); - // dependencies = JsonConvert.DeserializeObject>>(json); - } } } } diff --git a/Sample.Avalonia/rundevserver.sh b/Sample.Avalonia/rundevserver.sh new file mode 100644 index 00000000..f0fb2ea5 --- /dev/null +++ b/Sample.Avalonia/rundevserver.sh @@ -0,0 +1,12 @@ +#! /bin/bash +VIEW_GENERATOR_PATH=/Users/mpm/Development/git/O11IDE/packages/viewgenerator/1.1.17 +VIEW_PACKER_PATH=/Users/mpm/Development/git/O11IDE/packages/viewpacker/1.1.5 +PLUGINS_FOLDER=$~dp0../Plugins +PLUGIN_APPLICATION_CREATION_WIZARD=$PLUGINS_FOLDER/ApplicationCreationWizard/ServiceStudio +PLUGIN_CHROME_DEBUGGER_ADAPTER=$PLUGINS_FOLDER/ChromeDebuggerAdapter/ServiceStudio +FRAMEWORK_OUSYSTEMS_NODE_MODULES=/Users/mpm/Development/git/O11IDE/ServiceStudio/ServiceStudio.WebViewImplementation/../ServiceStudio.WebViewImplementation.Framework/node_modules/@outsystems +FRAMEWORK_OUSYSTEMS_DESIGN_SYSTEM=/Users/mpm/Development/git/O11IDE/ServiceStudio/ServiceStudio.WebViewImplementation/../ServiceStudio.WebViewImplementation.Framework/node_modules/@os-designsystem + +node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=true --env pluginsRelativePath='..' --env assemblyName="Sample.Avalonia" + +node "/Users/mpm/Development/git/ReactView/packages/viewpacker/1.1.5/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="/Users/mpm/Development/git/ReactView/packages/viewgenerator/1.1.17/tools/webpack/webpack_views.config.js" --env forHotReload=true --mode=development --devtool=inline-source-map --env useCache=true --env pluginsRelativePath='' --env assemblyName='Sample.Avalonia' \ No newline at end of file diff --git a/ViewGeneratorCore/tools/package-lock.json b/ViewGeneratorCore/tools/package-lock.json index d4452c1e..8f353610 100644 --- a/ViewGeneratorCore/tools/package-lock.json +++ b/ViewGeneratorCore/tools/package-lock.json @@ -1,6 +1,174 @@ { + "name": "tools", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "license": "ISC", + "dependencies": { + "@types/node": "^12.6.8" + }, + "devDependencies": { + "@outsystems/ts2lang": "1.0.21" + } + }, + "node_modules/@outsystems/ts2lang": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@outsystems/ts2lang/-/ts2lang-1.0.21.tgz", + "integrity": "sha512-K4v/hOvImzm/28ZpLZmiK3CRzdWm0SMJEN3mMbgKbj2rDAzLuAfE53RNE5DLAGoZGMeCrLkq6yuT9wLrAHANog==", + "dev": true, + "dependencies": { + "@outsystems/ts2lang": "^1.0.12", + "commander": "^2.9.0", + "glob": "^7.1.2", + "is-directory": "^0.3.1", + "merge": "^1.2.0", + "typescript": "3.4" + }, + "bin": { + "ts2lang": "ts2lang-main.js" + } + }, + "node_modules/@types/node": { + "version": "12.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.4.tgz", + "integrity": "sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ==" + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + }, "dependencies": { "@outsystems/ts2lang": { "version": "1.0.21", From bd6c4b5e882b1f33f4704e42ff2a91d010d9e9cb Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 15:37:21 +0000 Subject: [PATCH 04/33] fix encoding --- Sample.Avalonia/ExtendedReactView.cs | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index e85094a2..62c082b5 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -1,37 +1,37 @@ -using System; -using ReactViewControl; - -namespace Sample.Avalonia { - - public abstract class ExtendedReactView : ReactView { - - protected override ReactViewFactory Factory => new ExtendedReactViewFactory(); - - public ExtendedReactView(IViewModule mainModule) : base(mainModule) { - Settings.ThemeChanged += OnStylePreferenceChanged; - EmbeddedResourceRequested += OnEmbeddedResourceRequested; - mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; - } - - protected override void InnerDispose() { - base.InnerDispose(); - Settings.ThemeChanged -= OnStylePreferenceChanged; - } - - private void OnStylePreferenceChanged() { - RefreshDefaultStyleSheet(); - } - - private void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { - var resourceUrl = resourceHandler.Url; - - if (resourceUrl.Contains("ReactViewResources")) { - return; - } - - resourceUrl = new Uri(resourceUrl).PathAndQuery; - var devServerHost = new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); - resourceHandler.Redirect(new Uri(devServerHost, resourceUrl).ToString()); - } - } -} +using System; +using ReactViewControl; + +namespace Sample.Avalonia { + + public abstract class ExtendedReactView : ReactView { + + protected override ReactViewFactory Factory => new ExtendedReactViewFactory(); + + public ExtendedReactView(IViewModule mainModule) : base(mainModule) { + Settings.ThemeChanged += OnStylePreferenceChanged; + EmbeddedResourceRequested += OnEmbeddedResourceRequested; + mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; + } + + protected override void InnerDispose() { + base.InnerDispose(); + Settings.ThemeChanged -= OnStylePreferenceChanged; + } + + private void OnStylePreferenceChanged() { + RefreshDefaultStyleSheet(); + } + + private void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { + var resourceUrl = resourceHandler.Url; + + if (resourceUrl.Contains("ReactViewResources")) { + return; + } + + resourceUrl = new Uri(resourceUrl).PathAndQuery; + var devServerHost = new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); + resourceHandler.Redirect(new Uri(devServerHost, resourceUrl).ToString()); + } + } +} \ No newline at end of file From f7e8c05ae569bcea088d71728c71efc52965a1dd Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 15:41:04 +0000 Subject: [PATCH 05/33] fix encondings --- ReactViewControl/IViewModule.cs | 82 ++++++++++----------- Sample.Avalonia/ExtendedReactViewFactory.cs | 76 +++++++++---------- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index 058c5a5a..c4b2f92b 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; - -namespace ReactViewControl { - - public interface IViewModule { - - public string MainJsSource { get; } - - IModuleDependenciesProvider DependenciesProvider{ get; set; } - - string[] DependencyJsSources { get; } - - string[] CssSources { get; } - - string NativeObjectName { get; } - - string Name { get; } - - string Source { get; } - - object CreateNativeObject(); - - string[] Events { get; } - - KeyValuePair[] PropertiesValues { get; } - - void Bind(IFrame frame, IChildViewHost host = null); - - event CustomResourceRequestedEventHandler CustomResourceRequested; - - T WithPlugin(); - - void Load(); - - bool AddChildView(IViewModule childView, string frameName); - - T GetOrAddChildView(string frameName) where T : IViewModule, new(); - - ReactView Host { get; } - } -} +using System.Collections.Generic; + +namespace ReactViewControl { + + public interface IViewModule { + + public string MainJsSource { get; } + + IModuleDependenciesProvider DependenciesProvider{ get; set; } + + string[] DependencyJsSources { get; } + + string[] CssSources { get; } + + string NativeObjectName { get; } + + string Name { get; } + + string Source { get; } + + object CreateNativeObject(); + + string[] Events { get; } + + KeyValuePair[] PropertiesValues { get; } + + void Bind(IFrame frame, IChildViewHost host = null); + + event CustomResourceRequestedEventHandler CustomResourceRequested; + + T WithPlugin(); + + void Load(); + + bool AddChildView(IViewModule childView, string frameName); + + T GetOrAddChildView(string frameName) where T : IViewModule, new(); + + ReactView Host { get; } + } +} \ No newline at end of file diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 27d7f686..836c43af 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -1,38 +1,38 @@ -using System; -using ReactViewControl; -using WebViewControl; - -namespace Sample.Avalonia { - - internal class ExtendedReactViewFactory : ReactViewFactory { - private static WebPackDependenciesProvider provider = new WebPackDependenciesProvider(new Uri("http://localhost:8080/Sample.Avalonia/")); - - public override ResourceUrl DefaultStyleSheet => - new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); - - public override IViewModule[] InitializePlugins() { - var viewPlugin = new ViewPlugin(); -#if DEBUG - if (DevServerURI != null) { - viewPlugin.DependenciesProvider = ModuleDependenciesProvider; - } -#endif - return new[]{ - viewPlugin - }; - } - - public override bool ShowDeveloperTools => false; - - public override bool EnableViewPreload => true; - -#if DEBUG - public override bool EnableDebugMode => true; - - public override Uri DevServerURI => new Uri("http://localhost:8080/Sample.Avalonia/"); - - public override IModuleDependenciesProvider ModuleDependenciesProvider => - provider; -#endif - } -} +using System; +using ReactViewControl; +using WebViewControl; + +namespace Sample.Avalonia { + + internal class ExtendedReactViewFactory : ReactViewFactory { + private static WebPackDependenciesProvider provider = new WebPackDependenciesProvider(new Uri("http://localhost:8080/Sample.Avalonia/")); + + public override ResourceUrl DefaultStyleSheet => + new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); + + public override IViewModule[] InitializePlugins() { + var viewPlugin = new ViewPlugin(); +#if DEBUG + if (DevServerURI != null) { + viewPlugin.DependenciesProvider = ModuleDependenciesProvider; + } +#endif + return new[]{ + viewPlugin + }; + } + + public override bool ShowDeveloperTools => false; + + public override bool EnableViewPreload => true; + +#if DEBUG + public override bool EnableDebugMode => true; + + public override Uri DevServerURI => new Uri("http://localhost:8080/Sample.Avalonia/"); + + public override IModuleDependenciesProvider ModuleDependenciesProvider => + provider; +#endif + } +} \ No newline at end of file From cbfa588fd55cdac99a5cd8de28fed7b0f9166136 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 15:42:53 +0000 Subject: [PATCH 06/33] fix encoding --- ReactViewControl/ReactViewFactory.cs | 88 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/ReactViewControl/ReactViewFactory.cs b/ReactViewControl/ReactViewFactory.cs index 2560aa79..defc2f92 100644 --- a/ReactViewControl/ReactViewFactory.cs +++ b/ReactViewControl/ReactViewFactory.cs @@ -1,44 +1,44 @@ -using System; -using WebViewControl; - -namespace ReactViewControl { - - public class ReactViewFactory { - - /// - /// The default stylesheet. - /// - public virtual ResourceUrl DefaultStyleSheet => null; - - /// - /// Place plugins initialization here and return the plugins modules instances. - /// - /// - public virtual IViewModule[] InitializePlugins() => new IViewModule[0]; - - /// - /// Shows developers tools when the control is instantiated. - /// - public virtual bool ShowDeveloperTools => false; - - /// - /// Developer tools become available pressing F12. - /// - public virtual bool EnableDebugMode => false; - - /// - /// The view is cached and preloaded. First render occurs earlier. - /// - public virtual bool EnableViewPreload => true; - - /// - /// Webpack dev server url. Setting this value will enable hot reload. eg: new Uri("http://localhost:8080") - /// - public virtual Uri DevServerURI => null; - - /// - /// Module dependencies provider. - /// - public virtual IModuleDependenciesProvider ModuleDependenciesProvider => null; - } -} +using System; +using WebViewControl; + +namespace ReactViewControl { + + public class ReactViewFactory { + + /// + /// The default stylesheet. + /// + public virtual ResourceUrl DefaultStyleSheet => null; + + /// + /// Place plugins initialization here and return the plugins modules instances. + /// + /// + public virtual IViewModule[] InitializePlugins() => new IViewModule[0]; + + /// + /// Shows developers tools when the control is instantiated. + /// + public virtual bool ShowDeveloperTools => false; + + /// + /// Developer tools become available pressing F12. + /// + public virtual bool EnableDebugMode => false; + + /// + /// The view is cached and preloaded. First render occurs earlier. + /// + public virtual bool EnableViewPreload => true; + + /// + /// Webpack dev server url. Setting this value will enable hot reload. eg: new Uri("http://localhost:8080") + /// + public virtual Uri DevServerURI => null; + + /// + /// Module dependencies provider. + /// + public virtual IModuleDependenciesProvider ModuleDependenciesProvider => null; + } +} \ No newline at end of file From 4f4b01ad2c37e445b34db515478252ec1dcb96d0 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 15:45:39 +0000 Subject: [PATCH 07/33] fix encoding --- ReactViewControl/ReactViewRender.cs | 1276 +++++++++++++-------------- 1 file changed, 638 insertions(+), 638 deletions(-) diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 8cc8fccc..a3198557 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -1,639 +1,639 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using WebViewControl; -using Xilium.CefGlue; - -namespace ReactViewControl { - - internal partial class ReactViewRender : IChildViewHost, IDisposable { - -#if DEBUG - private static int counter; - private int id = counter++; -#endif - - private object SyncRoot { get; } = new object(); - - private const string CustomResourceBaseUrl = "resource"; - - private static Assembly ResourcesAssembly { get; } = typeof(ReactViewResources.Resources).Assembly; - - private Dictionary Frames { get; } = new Dictionary(); - private Dictionary> RecoverableFrames { get; } = new Dictionary>(); - - private ExtendedWebView WebView { get; } - private Assembly UserCallingAssembly { get; } - private LoaderModule Loader { get; } - private Func PluginsFactory { get; } - - private bool enableDebugMode; - private ResourceUrl defaultStyleSheet; - private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) - - public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null, IModuleDependenciesProvider moduleDependenciesProvider = null) { - UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; - - // must useSharedDomain for the local storage to be shared - WebView = new ExtendedWebView(useSharedDomain: true) { - DisableBuiltinContextMenus = true, - IsSecurityDisabled = true, - IgnoreMissingResources = false, - IsHistoryDisabled = true - }; - - NativeAPI.Initialize(this); - Loader = new LoaderModule(this); - - DefaultStyleSheet = defaultStyleSheet; - PluginsFactory = initializePlugins; - EnableDebugMode = enableDebugMode; - DevServerUri = devServerUri; - ModuleDependenciesProvider = moduleDependenciesProvider; - - GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame - - WebView.Disposed += Dispose; - WebView.BeforeNavigate += OnWebViewBeforeNavigate; - WebView.BeforeResourceLoad += OnWebViewBeforeResourceLoad; - WebView.LoadFailed += OnWebViewLoadFailed; - WebView.FilesDragging += fileNames => FilesDragging?.Invoke(fileNames); - WebView.TextDragging += textContent => TextDragging?.Invoke(textContent); - WebView.KeyPressed += OnWebViewKeyPressed; - - ExtraInitialize(); - - var urlParams = new string[] { - new ResourceUrl(ResourcesAssembly).ToString(), - enableDebugMode ? "true" : "false", - ExecutionEngine.ModulesObjectName, - NativeAPI.NativeObjectName, - ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl - }; - - WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams))); - - if (preloadWebView) { - PreloadWebView(); - } - - EditCommands = new EditCommands(WebView); - } - - partial void ExtraInitialize(); - - partial void PreloadWebView(); - - public ReactView Host { get; set; } - - /// - /// True when hot reload is enabled. - /// - public bool IsHotReloadEnabled => DevServerUri != null; - - public bool IsDisposing => WebView.IsDisposing; - - /// - /// True when the main component has been rendered. - /// - public bool IsReady => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus == LoadStatus.Ready; - - /// - /// True when view component is loading or loaded. - /// - public bool IsMainComponentLoaded => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus >= LoadStatus.ComponentLoading; - - /// - /// Enables or disables debug mode. - /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details - /// when a resource fails to load. - /// - public bool EnableDebugMode { - get { return enableDebugMode; } - set { - enableDebugMode = value; - WebView.AllowDeveloperTools = enableDebugMode; - if (enableDebugMode) { - WebView.ResourceLoadFailed += Loader.ShowResourceLoadFailedMessage; - } else { - WebView.ResourceLoadFailed -= Loader.ShowResourceLoadFailedMessage; - } - } - } - - /// - /// Gets webpack dev server url. - /// - public Uri DevServerUri { get; } - - public IModuleDependenciesProvider ModuleDependenciesProvider { get; } - - - /// - /// Gets or sets the webview zoom percentage (1 = 100%) - /// - public double ZoomPercentage { - get { return WebView.ZoomPercentage; } - set { WebView.ZoomPercentage = value; } - } - - /// - /// Event fired when the component is rendered and ready for interaction. - /// - public event Action Ready; - - /// - /// Event fired when an async exception occurs (eg: executing javascript) - /// - public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { - add { WebView.UnhandledAsyncException += value; } - remove { WebView.UnhandledAsyncException -= value; } - } - - /// - /// Event fired when a resource fails to load. - /// - public event ResourceLoadFailedEventHandler ResourceLoadFailed { - add { WebView.ResourceLoadFailed += value; } - remove { WebView.ResourceLoadFailed -= value; } - } - - /// - /// Handle embedded resource requests. You can use this event to change the resource being loaded. - /// - public event ResourceRequestedEventHandler EmbeddedResourceRequested; - - /// - /// Handle external resource requests. - /// Call to handle the request in an async way. - /// - public event ResourceRequestedEventHandler ExternalResourceRequested; - - /// - /// Handle custom resource requests. Use this event to load the resource based on provided key. - /// - public event CustomResourceRequestedEventHandler CustomResourceRequested; - - /// - /// Handle drag of files. Use this event to get the full path of the files being dragged. - /// - internal event FilesDraggingEventHandler FilesDragging; - - /// - /// Handle drag of text. Use this event to get the text content being dragged. - /// - internal event TextDraggingEventHandler TextDragging; - - /// - /// Gets the edition commands. - /// - internal EditCommands EditCommands { get; } - - /// - /// Javascript context was destroyed, cleanup everything. - /// - /// - private void OnWebViewJavascriptContextReleased(string frameName) { - if (!WebView.IsMainFrame(frameName)) { - // ignore, its an iframe saying goodbye - return; - } - - lock (SyncRoot) { - var mainFrame = Frames[FrameInfo.MainViewFrameName]; - - Frames.Remove(mainFrame.Name); - RecoverableFrames.Clear(); - foreach (var keyValuePair in Frames) { - RecoverableFrames[keyValuePair.Key] = new WeakReference(keyValuePair.Value); - UnregisterNativeObject(keyValuePair.Value.Component, keyValuePair.Value); - } - - Frames.Clear(); - Frames.Add(mainFrame.Name, mainFrame); - var previousComponentReady = mainFrame.IsComponentReadyToLoad; - mainFrame.Reset(); - mainFrame.IsComponentReadyToLoad = previousComponentReady; - } - } - - public void Dispose() { - WebView.Dispose(); - } - - /// - /// Initialize the underlying webview if has been initialized yet. - /// - public void EnsureInitialized() { - if (!WebView.IsBrowserInitialized) { - PreloadWebView(); - } - } - - /// - /// Binds the specified component to the main frame. - /// - /// - public void BindComponent(IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - BindComponentToFrame(component, frame); - } - } - - /// - /// Load the specified component into the main frame. - /// - /// - public void LoadComponent(IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - frame.IsComponentReadyToLoad = true; - TryLoadComponent(frame); - } - } - - void IChildViewHost.LoadComponent(string frameName, IViewModule component) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - // component not bound yet? bind it - BindComponentToFrame(component, frame); - } - frame.IsComponentReadyToLoad = true; - TryLoadComponent(frame); - } - } - - /// - /// Loads the frame component. - /// - /// - private void TryLoadComponent(FrameInfo frame) { - if (frame.Component == null || frame.LoadStatus != LoadStatus.ViewInitialized || !frame.IsComponentReadyToLoad) { - return; - } - - frame.LoadStatus = LoadStatus.ComponentLoading; - - RegisterNativeObject(frame.Component, frame); - - if (ModuleDependenciesProvider != null) { - frame.Component.DependenciesProvider = ModuleDependenciesProvider; - } - - Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); - if (isInputDisabled && frame.IsMain) { - Loader.DisableMouseInteractions(); - } - } - - /// - /// Gets or sets the url of the default stylesheet. - /// - public ResourceUrl DefaultStyleSheet { - get { return defaultStyleSheet; } - set { - if (IsMainComponentLoaded) { - Loader.LoadDefaultStyleSheet(value); - } - defaultStyleSheet = value; - } - } - - private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { - var invalidPlugins = plugins.Where(p => string.IsNullOrEmpty(p.MainJsSource) || string.IsNullOrEmpty(p.Name) || string.IsNullOrEmpty(p.NativeObjectName)); - if (invalidPlugins.Any()) { - var pluginName = invalidPlugins.First().Name + "|" + invalidPlugins.First().GetType().Name; - throw new ArgumentException($"Plugin '{pluginName}' is invalid"); - } - - if (frame.LoadStatus > LoadStatus.ViewInitialized) { - throw new InvalidOperationException($"Cannot add plugins after component has been loaded"); - } - - frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); - - foreach (var plugin in plugins) { - plugin.Bind(frame); - } - } - - /// - /// Retrieves the specified plugin module instance for the spcifies frame. - /// - /// Type of the plugin to retrieve. - /// - /// If the plugin hasn't been registered on the specified frame. - /// - public T WithPlugin(string frameName = FrameInfo.MainViewFrameName) { - if (!Frames.TryGetValue(frameName, out var frame)) { - throw new InvalidOperationException($"Frame {frameName} is not loaded"); - } - - return frame.GetPlugin(); - } - - /// - /// Opens the developer tools. - /// - public void ShowDeveloperTools() { - WebView.ShowDeveloperTools(); - } - - /// - /// Closes the developer tools. - /// - public void CloseDeveloperTools() { - WebView.CloseDeveloperTools(); - } - - /// - /// Add an handler for custom resources from the specified frame. - /// - /// - /// - public void AddCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - frame.CustomResourceRequestedHandler += handler; - } - } - - /// - /// Remve the handler for custom resources from the specified frame. - /// - /// - /// - public void RemoveCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { - // do not create if frame does not exist - if (Frames.TryGetValue(frameName, out var frame)) { - frame.CustomResourceRequestedHandler -= handler; - } - } - - /// - /// Gets the view loaded on the specified frame. If none it will create a view of the specified - /// instance and bind it to the frame. - /// - /// - /// - public T GetOrAddChildView(string frameName) where T : IViewModule, new() { - T component; - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - component = new T(); - BindComponentToFrame(component, frame); - } else { - component = (T)frame.Component; - } - } - - return component; - } - - /// - /// Adds the specified view instance and binds it to the frame. - /// - /// - /// True if the view was bound to the frame. False if the frame already as a component bound. - public bool AddChildView(IViewModule childView, string frameName) { - lock (SyncRoot) { - var frame = GetOrCreateFrame(frameName); - if (frame.Component == null) { - BindComponentToFrame(childView, frame); - return true; - } - return false; - } - } - - /// - /// Binds the component to the specified frame. - /// - /// - /// - private void BindComponentToFrame(IViewModule component, FrameInfo frame) { - frame.Component = component; - component.Bind(frame, this); - } - - /// - /// Handles webview url load request. - /// - /// - private void OnWebViewBeforeNavigate(Request request) { - if (request.IsMainFrame && !request.Url.OrdinalStartsWith($"{ResourceUrl.EmbeddedScheme}{Uri.SchemeDelimiter}")) { - UrlHelper.OpenInExternalBrowser(request.Url); - request.Cancel(); - } - } - - /// - /// Handles the webview load of resources - /// - /// - private void OnWebViewBeforeResourceLoad(ResourceHandler resourceHandler) { - var url = resourceHandler.Url; - var scheme = url.Substring(0, Math.Max(0, url.OrdinalIndexOf(Uri.SchemeDelimiter))); - - switch (scheme.ToLowerInvariant()) { - case ResourceUrl.CustomScheme: - HandleCustomResourceRequested(resourceHandler); - break; - - case ResourceUrl.EmbeddedScheme: - // webview already started BeginAsyncResponse - EmbeddedResourceRequested?.Invoke(resourceHandler); - break; - - case "http": - case "https": - ExternalResourceRequested?.Invoke(resourceHandler); - break; - } - } - - /// - /// Handles webview load errors. - /// - /// - /// - /// The iframe name, not to be confused with view frame name - private void OnWebViewLoadFailed(string url, int errorCode, string frameName) { - if (!WebView.IsMainFrame(frameName)) { - // ignore errors in iframes - return; - } - - throw new Exception($"Failed to load view (error: {errorCode})"); - } - - /// - /// Handles webview keypresses. - /// - /// - /// - private void OnWebViewKeyPressed(CefKeyEvent keyEvent, out bool handled) { - handled = isInputDisabled; - } - - private CustomResourceRequestedEventHandler[] GetCustomResourceHandlers(FrameInfo frame) { - var globalHandlers = CustomResourceRequested?.GetInvocationList().Cast() ?? Enumerable.Empty(); - var frameHandlers = frame.CustomResourceRequestedHandler?.GetInvocationList().Cast() ?? Enumerable.Empty(); - return globalHandlers.Concat(frameHandlers).ToArray(); - } - - /// - /// Handle custom resource request and forward it to the appropriate frame. - /// - /// - /// - private void HandleCustomResourceRequested(ResourceHandler resourceHandler) { - var url = resourceHandler.Url; - - if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Segments.Length > 1 && uri.Host.Equals(CustomResourceBaseUrl, StringComparison.InvariantCultureIgnoreCase)) { - var frameName = uri.Segments.ElementAt(1).TrimEnd(ResourceUrl.PathSeparator.ToCharArray()); - if (frameName != null && Frames.TryGetValue(frameName, out var frame)) { - var customResourceRequestedHandlers = GetCustomResourceHandlers(frame); - if (customResourceRequestedHandlers.Any()) { - resourceHandler.BeginAsyncResponse(() => { - // get resource key from the query params - var resourceKeyAndOptions = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(p => Uri.UnescapeDataString(p)); - var resourceKey = resourceKeyAndOptions.FirstOrDefault(); - var options = resourceKeyAndOptions.Skip(1).ToArray(); - - // get response from first handler that returns a stream - var response = customResourceRequestedHandlers.Select(h => h(resourceKey, options)).FirstOrDefault(r => r?.Content != null); - - if (response != null) { - var extension = (response.Extension ?? Path.GetExtension(resourceKey)).TrimStart('.'); - resourceHandler.RespondWith(response.Content, extension); - } else { - resourceHandler.RespondWith(MemoryStream.Null); - } - }); - } - } - } - } - - /// - /// Registers a .net object to be available on the js context. - /// - /// - /// - /// - private void RegisterNativeObject(IViewModule module, FrameInfo frame) { - var nativeObjectName = module.GetNativeObjectFullName(frame.Name); - WebView.RegisterJavascriptObject(nativeObjectName, module.CreateNativeObject(), interceptCall: CallNativeMethod); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Task CallNativeMethod(Func nativeMethod) { - if (Host != null) { - return Host.CallNativeMethod(nativeMethod); - } - return Task.FromResult(nativeMethod()); - } - - /// - /// Unregisters a .net object available on the js context. - /// - /// - /// - private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { - var nativeObjectName = module.GetNativeObjectFullName(frame.Name); - WebView.UnregisterJavascriptObject(nativeObjectName); - } - - /// - /// Converts an url to a full path url - /// - /// - /// - private string ToFullUrl(string url) { - // TODO: BUG AQUI - if (url.OrdinalContains(Uri.SchemeDelimiter)) { - return url; - } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { - if (IsHotReloadEnabled) { - return new Uri(DevServerUri, url).ToString(); - } else { - return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); - } - } else { - return new ResourceUrl(UserCallingAssembly, url).ToString(); - } - } - - /// - /// Normalizes the url path separators - /// - /// - /// - private static string NormalizeUrl(string url) { - return url.Replace("\\", ResourceUrl.PathSeparator); - } - - /// - /// Disables or enables keyboard and mouse interactions with the browser - /// - /// - private void DisableInputInteractions(bool disable) { - if (isInputDisabled == disable) { - return; - } - lock (SyncRoot) { - if (isInputDisabled == disable) { - return; - } - isInputDisabled = disable; - var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); - if (frame.LoadStatus >= LoadStatus.ComponentLoading) { - if (disable) { - Loader.DisableMouseInteractions(); - } else { - Loader.EnableMouseInteractions(); - } - } - } - } - - private FrameInfo GetOrCreateFrame(string frameName) { - if (Frames.TryGetValue(frameName, out var frame)) { - return frame; - } - - if (RecoverableFrames.TryGetValue(frameName, out var weakReferenceFrame)) { - RecoverableFrames.Remove(frameName); - if (weakReferenceFrame.TryGetTarget(out var recoverableFrame)) { - Frames[frameName] = recoverableFrame; - return recoverableFrame; - } - } - - var newFrame = new FrameInfo(frameName); - Frames[frameName] = newFrame; - AddPlugins(PluginsFactory(), newFrame); - - return newFrame; - } - - private static MethodBase GetUserCallingMethod(bool captureFilenames = false) { - var currentAssembly = typeof(ReactView).Assembly; - var callstack = new StackTrace(captureFilenames).GetFrames().Select(f => f.GetMethod()).Where(m => m.ReflectedType.Assembly != currentAssembly); - var userMethod = callstack.First(m => !IsFrameworkAssemblyName(m.ReflectedType.Assembly.GetName().Name)); - if (userMethod == null) { - throw new InvalidOperationException("Unable to find calling method"); - } - return userMethod; - } - } +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using WebViewControl; +using Xilium.CefGlue; + +namespace ReactViewControl { + + internal partial class ReactViewRender : IChildViewHost, IDisposable { + +#if DEBUG + private static int counter; + private int id = counter++; +#endif + + private object SyncRoot { get; } = new object(); + + private const string CustomResourceBaseUrl = "resource"; + + private static Assembly ResourcesAssembly { get; } = typeof(ReactViewResources.Resources).Assembly; + + private Dictionary Frames { get; } = new Dictionary(); + private Dictionary> RecoverableFrames { get; } = new Dictionary>(); + + private ExtendedWebView WebView { get; } + private Assembly UserCallingAssembly { get; } + private LoaderModule Loader { get; } + private Func PluginsFactory { get; } + + private bool enableDebugMode; + private ResourceUrl defaultStyleSheet; + private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) + + public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null, IModuleDependenciesProvider moduleDependenciesProvider = null) { + UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; + + // must useSharedDomain for the local storage to be shared + WebView = new ExtendedWebView(useSharedDomain: true) { + DisableBuiltinContextMenus = true, + IsSecurityDisabled = true, + IgnoreMissingResources = false, + IsHistoryDisabled = true + }; + + NativeAPI.Initialize(this); + Loader = new LoaderModule(this); + + DefaultStyleSheet = defaultStyleSheet; + PluginsFactory = initializePlugins; + EnableDebugMode = enableDebugMode; + DevServerUri = devServerUri; + ModuleDependenciesProvider = moduleDependenciesProvider; + + GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame + + WebView.Disposed += Dispose; + WebView.BeforeNavigate += OnWebViewBeforeNavigate; + WebView.BeforeResourceLoad += OnWebViewBeforeResourceLoad; + WebView.LoadFailed += OnWebViewLoadFailed; + WebView.FilesDragging += fileNames => FilesDragging?.Invoke(fileNames); + WebView.TextDragging += textContent => TextDragging?.Invoke(textContent); + WebView.KeyPressed += OnWebViewKeyPressed; + + ExtraInitialize(); + + var urlParams = new string[] { + new ResourceUrl(ResourcesAssembly).ToString(), + enableDebugMode ? "true" : "false", + ExecutionEngine.ModulesObjectName, + NativeAPI.NativeObjectName, + ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl + }; + + WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams))); + + if (preloadWebView) { + PreloadWebView(); + } + + EditCommands = new EditCommands(WebView); + } + + partial void ExtraInitialize(); + + partial void PreloadWebView(); + + public ReactView Host { get; set; } + + /// + /// True when hot reload is enabled. + /// + public bool IsHotReloadEnabled => DevServerUri != null; + + public bool IsDisposing => WebView.IsDisposing; + + /// + /// True when the main component has been rendered. + /// + public bool IsReady => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus == LoadStatus.Ready; + + /// + /// True when view component is loading or loaded. + /// + public bool IsMainComponentLoaded => Frames.TryGetValue(FrameInfo.MainViewFrameName, out var frame) && frame.LoadStatus >= LoadStatus.ComponentLoading; + + /// + /// Enables or disables debug mode. + /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details + /// when a resource fails to load. + /// + public bool EnableDebugMode { + get { return enableDebugMode; } + set { + enableDebugMode = value; + WebView.AllowDeveloperTools = enableDebugMode; + if (enableDebugMode) { + WebView.ResourceLoadFailed += Loader.ShowResourceLoadFailedMessage; + } else { + WebView.ResourceLoadFailed -= Loader.ShowResourceLoadFailedMessage; + } + } + } + + /// + /// Gets webpack dev server url. + /// + public Uri DevServerUri { get; } + + public IModuleDependenciesProvider ModuleDependenciesProvider { get; } + + + /// + /// Gets or sets the webview zoom percentage (1 = 100%) + /// + public double ZoomPercentage { + get { return WebView.ZoomPercentage; } + set { WebView.ZoomPercentage = value; } + } + + /// + /// Event fired when the component is rendered and ready for interaction. + /// + public event Action Ready; + + /// + /// Event fired when an async exception occurs (eg: executing javascript) + /// + public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { + add { WebView.UnhandledAsyncException += value; } + remove { WebView.UnhandledAsyncException -= value; } + } + + /// + /// Event fired when a resource fails to load. + /// + public event ResourceLoadFailedEventHandler ResourceLoadFailed { + add { WebView.ResourceLoadFailed += value; } + remove { WebView.ResourceLoadFailed -= value; } + } + + /// + /// Handle embedded resource requests. You can use this event to change the resource being loaded. + /// + public event ResourceRequestedEventHandler EmbeddedResourceRequested; + + /// + /// Handle external resource requests. + /// Call to handle the request in an async way. + /// + public event ResourceRequestedEventHandler ExternalResourceRequested; + + /// + /// Handle custom resource requests. Use this event to load the resource based on provided key. + /// + public event CustomResourceRequestedEventHandler CustomResourceRequested; + + /// + /// Handle drag of files. Use this event to get the full path of the files being dragged. + /// + internal event FilesDraggingEventHandler FilesDragging; + + /// + /// Handle drag of text. Use this event to get the text content being dragged. + /// + internal event TextDraggingEventHandler TextDragging; + + /// + /// Gets the edition commands. + /// + internal EditCommands EditCommands { get; } + + /// + /// Javascript context was destroyed, cleanup everything. + /// + /// + private void OnWebViewJavascriptContextReleased(string frameName) { + if (!WebView.IsMainFrame(frameName)) { + // ignore, its an iframe saying goodbye + return; + } + + lock (SyncRoot) { + var mainFrame = Frames[FrameInfo.MainViewFrameName]; + + Frames.Remove(mainFrame.Name); + RecoverableFrames.Clear(); + foreach (var keyValuePair in Frames) { + RecoverableFrames[keyValuePair.Key] = new WeakReference(keyValuePair.Value); + UnregisterNativeObject(keyValuePair.Value.Component, keyValuePair.Value); + } + + Frames.Clear(); + Frames.Add(mainFrame.Name, mainFrame); + var previousComponentReady = mainFrame.IsComponentReadyToLoad; + mainFrame.Reset(); + mainFrame.IsComponentReadyToLoad = previousComponentReady; + } + } + + public void Dispose() { + WebView.Dispose(); + } + + /// + /// Initialize the underlying webview if has been initialized yet. + /// + public void EnsureInitialized() { + if (!WebView.IsBrowserInitialized) { + PreloadWebView(); + } + } + + /// + /// Binds the specified component to the main frame. + /// + /// + public void BindComponent(IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + BindComponentToFrame(component, frame); + } + } + + /// + /// Load the specified component into the main frame. + /// + /// + public void LoadComponent(IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + frame.IsComponentReadyToLoad = true; + TryLoadComponent(frame); + } + } + + void IChildViewHost.LoadComponent(string frameName, IViewModule component) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + // component not bound yet? bind it + BindComponentToFrame(component, frame); + } + frame.IsComponentReadyToLoad = true; + TryLoadComponent(frame); + } + } + + /// + /// Loads the frame component. + /// + /// + private void TryLoadComponent(FrameInfo frame) { + if (frame.Component == null || frame.LoadStatus != LoadStatus.ViewInitialized || !frame.IsComponentReadyToLoad) { + return; + } + + frame.LoadStatus = LoadStatus.ComponentLoading; + + RegisterNativeObject(frame.Component, frame); + + if (ModuleDependenciesProvider != null) { + frame.Component.DependenciesProvider = ModuleDependenciesProvider; + } + + Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); + if (isInputDisabled && frame.IsMain) { + Loader.DisableMouseInteractions(); + } + } + + /// + /// Gets or sets the url of the default stylesheet. + /// + public ResourceUrl DefaultStyleSheet { + get { return defaultStyleSheet; } + set { + if (IsMainComponentLoaded) { + Loader.LoadDefaultStyleSheet(value); + } + defaultStyleSheet = value; + } + } + + private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { + var invalidPlugins = plugins.Where(p => string.IsNullOrEmpty(p.MainJsSource) || string.IsNullOrEmpty(p.Name) || string.IsNullOrEmpty(p.NativeObjectName)); + if (invalidPlugins.Any()) { + var pluginName = invalidPlugins.First().Name + "|" + invalidPlugins.First().GetType().Name; + throw new ArgumentException($"Plugin '{pluginName}' is invalid"); + } + + if (frame.LoadStatus > LoadStatus.ViewInitialized) { + throw new InvalidOperationException($"Cannot add plugins after component has been loaded"); + } + + frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); + + foreach (var plugin in plugins) { + plugin.Bind(frame); + } + } + + /// + /// Retrieves the specified plugin module instance for the spcifies frame. + /// + /// Type of the plugin to retrieve. + /// + /// If the plugin hasn't been registered on the specified frame. + /// + public T WithPlugin(string frameName = FrameInfo.MainViewFrameName) { + if (!Frames.TryGetValue(frameName, out var frame)) { + throw new InvalidOperationException($"Frame {frameName} is not loaded"); + } + + return frame.GetPlugin(); + } + + /// + /// Opens the developer tools. + /// + public void ShowDeveloperTools() { + WebView.ShowDeveloperTools(); + } + + /// + /// Closes the developer tools. + /// + public void CloseDeveloperTools() { + WebView.CloseDeveloperTools(); + } + + /// + /// Add an handler for custom resources from the specified frame. + /// + /// + /// + public void AddCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + frame.CustomResourceRequestedHandler += handler; + } + } + + /// + /// Remve the handler for custom resources from the specified frame. + /// + /// + /// + public void RemoveCustomResourceRequestedHandler(string frameName, CustomResourceRequestedEventHandler handler) { + // do not create if frame does not exist + if (Frames.TryGetValue(frameName, out var frame)) { + frame.CustomResourceRequestedHandler -= handler; + } + } + + /// + /// Gets the view loaded on the specified frame. If none it will create a view of the specified + /// instance and bind it to the frame. + /// + /// + /// + public T GetOrAddChildView(string frameName) where T : IViewModule, new() { + T component; + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + component = new T(); + BindComponentToFrame(component, frame); + } else { + component = (T)frame.Component; + } + } + + return component; + } + + /// + /// Adds the specified view instance and binds it to the frame. + /// + /// + /// True if the view was bound to the frame. False if the frame already as a component bound. + public bool AddChildView(IViewModule childView, string frameName) { + lock (SyncRoot) { + var frame = GetOrCreateFrame(frameName); + if (frame.Component == null) { + BindComponentToFrame(childView, frame); + return true; + } + return false; + } + } + + /// + /// Binds the component to the specified frame. + /// + /// + /// + private void BindComponentToFrame(IViewModule component, FrameInfo frame) { + frame.Component = component; + component.Bind(frame, this); + } + + /// + /// Handles webview url load request. + /// + /// + private void OnWebViewBeforeNavigate(Request request) { + if (request.IsMainFrame && !request.Url.OrdinalStartsWith($"{ResourceUrl.EmbeddedScheme}{Uri.SchemeDelimiter}")) { + UrlHelper.OpenInExternalBrowser(request.Url); + request.Cancel(); + } + } + + /// + /// Handles the webview load of resources + /// + /// + private void OnWebViewBeforeResourceLoad(ResourceHandler resourceHandler) { + var url = resourceHandler.Url; + var scheme = url.Substring(0, Math.Max(0, url.OrdinalIndexOf(Uri.SchemeDelimiter))); + + switch (scheme.ToLowerInvariant()) { + case ResourceUrl.CustomScheme: + HandleCustomResourceRequested(resourceHandler); + break; + + case ResourceUrl.EmbeddedScheme: + // webview already started BeginAsyncResponse + EmbeddedResourceRequested?.Invoke(resourceHandler); + break; + + case "http": + case "https": + ExternalResourceRequested?.Invoke(resourceHandler); + break; + } + } + + /// + /// Handles webview load errors. + /// + /// + /// + /// The iframe name, not to be confused with view frame name + private void OnWebViewLoadFailed(string url, int errorCode, string frameName) { + if (!WebView.IsMainFrame(frameName)) { + // ignore errors in iframes + return; + } + + throw new Exception($"Failed to load view (error: {errorCode})"); + } + + /// + /// Handles webview keypresses. + /// + /// + /// + private void OnWebViewKeyPressed(CefKeyEvent keyEvent, out bool handled) { + handled = isInputDisabled; + } + + private CustomResourceRequestedEventHandler[] GetCustomResourceHandlers(FrameInfo frame) { + var globalHandlers = CustomResourceRequested?.GetInvocationList().Cast() ?? Enumerable.Empty(); + var frameHandlers = frame.CustomResourceRequestedHandler?.GetInvocationList().Cast() ?? Enumerable.Empty(); + return globalHandlers.Concat(frameHandlers).ToArray(); + } + + /// + /// Handle custom resource request and forward it to the appropriate frame. + /// + /// + /// + private void HandleCustomResourceRequested(ResourceHandler resourceHandler) { + var url = resourceHandler.Url; + + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Segments.Length > 1 && uri.Host.Equals(CustomResourceBaseUrl, StringComparison.InvariantCultureIgnoreCase)) { + var frameName = uri.Segments.ElementAt(1).TrimEnd(ResourceUrl.PathSeparator.ToCharArray()); + if (frameName != null && Frames.TryGetValue(frameName, out var frame)) { + var customResourceRequestedHandlers = GetCustomResourceHandlers(frame); + if (customResourceRequestedHandlers.Any()) { + resourceHandler.BeginAsyncResponse(() => { + // get resource key from the query params + var resourceKeyAndOptions = uri.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).Select(p => Uri.UnescapeDataString(p)); + var resourceKey = resourceKeyAndOptions.FirstOrDefault(); + var options = resourceKeyAndOptions.Skip(1).ToArray(); + + // get response from first handler that returns a stream + var response = customResourceRequestedHandlers.Select(h => h(resourceKey, options)).FirstOrDefault(r => r?.Content != null); + + if (response != null) { + var extension = (response.Extension ?? Path.GetExtension(resourceKey)).TrimStart('.'); + resourceHandler.RespondWith(response.Content, extension); + } else { + resourceHandler.RespondWith(MemoryStream.Null); + } + }); + } + } + } + } + + /// + /// Registers a .net object to be available on the js context. + /// + /// + /// + /// + private void RegisterNativeObject(IViewModule module, FrameInfo frame) { + var nativeObjectName = module.GetNativeObjectFullName(frame.Name); + WebView.RegisterJavascriptObject(nativeObjectName, module.CreateNativeObject(), interceptCall: CallNativeMethod); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Task CallNativeMethod(Func nativeMethod) { + if (Host != null) { + return Host.CallNativeMethod(nativeMethod); + } + return Task.FromResult(nativeMethod()); + } + + /// + /// Unregisters a .net object available on the js context. + /// + /// + /// + private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { + var nativeObjectName = module.GetNativeObjectFullName(frame.Name); + WebView.UnregisterJavascriptObject(nativeObjectName); + } + + /// + /// Converts an url to a full path url + /// + /// + /// + private string ToFullUrl(string url) { + // TODO: BUG AQUI + if (url.OrdinalContains(Uri.SchemeDelimiter)) { + return url; + } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { + if (IsHotReloadEnabled) { + return new Uri(DevServerUri, url).ToString(); + } else { + return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); + } + } else { + return new ResourceUrl(UserCallingAssembly, url).ToString(); + } + } + + /// + /// Normalizes the url path separators + /// + /// + /// + private static string NormalizeUrl(string url) { + return url.Replace("\\", ResourceUrl.PathSeparator); + } + + /// + /// Disables or enables keyboard and mouse interactions with the browser + /// + /// + private void DisableInputInteractions(bool disable) { + if (isInputDisabled == disable) { + return; + } + lock (SyncRoot) { + if (isInputDisabled == disable) { + return; + } + isInputDisabled = disable; + var frame = GetOrCreateFrame(FrameInfo.MainViewFrameName); + if (frame.LoadStatus >= LoadStatus.ComponentLoading) { + if (disable) { + Loader.DisableMouseInteractions(); + } else { + Loader.EnableMouseInteractions(); + } + } + } + } + + private FrameInfo GetOrCreateFrame(string frameName) { + if (Frames.TryGetValue(frameName, out var frame)) { + return frame; + } + + if (RecoverableFrames.TryGetValue(frameName, out var weakReferenceFrame)) { + RecoverableFrames.Remove(frameName); + if (weakReferenceFrame.TryGetTarget(out var recoverableFrame)) { + Frames[frameName] = recoverableFrame; + return recoverableFrame; + } + } + + var newFrame = new FrameInfo(frameName); + Frames[frameName] = newFrame; + AddPlugins(PluginsFactory(), newFrame); + + return newFrame; + } + + private static MethodBase GetUserCallingMethod(bool captureFilenames = false) { + var currentAssembly = typeof(ReactView).Assembly; + var callstack = new StackTrace(captureFilenames).GetFrames().Select(f => f.GetMethod()).Where(m => m.ReflectedType.Assembly != currentAssembly); + var userMethod = callstack.First(m => !IsFrameworkAssemblyName(m.ReflectedType.Assembly.GetName().Name)); + if (userMethod == null) { + throw new InvalidOperationException("Unable to find calling method"); + } + return userMethod; + } + } } \ No newline at end of file From a4b9158c5e647e1398f44b5db96f88d9ca07c9d8 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Mon, 30 Oct 2023 15:47:09 +0000 Subject: [PATCH 08/33] fix encoding --- ReactViewControl/ViewModuleContainer.cs | 204 ++++++++++++------------ 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index b5899afd..019a41ca 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -1,102 +1,102 @@ -using System; -using System.Collections.Generic; - -namespace ReactViewControl { - - public abstract class ViewModuleContainer : IViewModule { - - private const string JsEntryFileExtension = ".js.entry"; - private const string CssEntryFileExtension = ".css.entry"; - - private IFrame frame; - private IChildViewHost childViewHost; - private IModuleDependenciesProvider dependenciesProvider; - - public ViewModuleContainer() { - frame = new FrameInfo("dummy"); - } - - public virtual IModuleDependenciesProvider DependenciesProvider { - get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } - set { dependenciesProvider = value; } - } - - protected virtual string MainJsSource => null; - protected virtual string NativeObjectName => null; - protected virtual string ModuleName => null; - protected virtual string Source => null; - - protected virtual object CreateNativeObject() => null; - - protected virtual string[] Events => new string[0]; - - protected virtual KeyValuePair[] PropertiesValues => new KeyValuePair[0]; - - string IViewModule.MainJsSource => MainJsSource; - - string IViewModule.NativeObjectName => NativeObjectName; - - string IViewModule.Name => ModuleName; - - string IViewModule.Source => Source; - - object IViewModule.CreateNativeObject() => CreateNativeObject(); - - string[] IViewModule.Events => Events; - - string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(MainJsSource); - - string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(MainJsSource); - - KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; - - void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { - frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; - frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); - this.frame = frame; - this.childViewHost = childViewHost; - } - - // ease access in generated code - protected IExecutionEngine ExecutionEngine { - get { - var engine = frame.ExecutionEngine; - if (engine == null) { - throw new InvalidOperationException("View module must be bound to an execution engine"); - } - return engine; - } - } - - public event CustomResourceRequestedEventHandler CustomResourceRequested { - add => frame.CustomResourceRequestedHandler += value; - remove => frame.CustomResourceRequestedHandler -= value; - } - - public T WithPlugin() { - return frame.GetPlugin(); - } - - public void Load() { - childViewHost?.LoadComponent(frame.Name, this); - } - - public T GetOrAddChildView(string frameName) where T : IViewModule, new() { - if (childViewHost == null) { - return default(T); - } - return childViewHost.GetOrAddChildView(FormatChildViewName(frameName)); - } - - public bool AddChildView(IViewModule childView, string frameName) { - if (childViewHost == null) { - return false; - } - return childViewHost.AddChildView(childView, FormatChildViewName(frameName)); - } - - public ReactView Host => childViewHost?.Host; - - private string FormatChildViewName(string name) => frame.Name + (string.IsNullOrEmpty(frame.Name) ? "" : ".") + name; - } -} +using System; +using System.Collections.Generic; + +namespace ReactViewControl { + + public abstract class ViewModuleContainer : IViewModule { + + private const string JsEntryFileExtension = ".js.entry"; + private const string CssEntryFileExtension = ".css.entry"; + + private IFrame frame; + private IChildViewHost childViewHost; + private IModuleDependenciesProvider dependenciesProvider; + + public ViewModuleContainer() { + frame = new FrameInfo("dummy"); + } + + public virtual IModuleDependenciesProvider DependenciesProvider { + get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } + set { dependenciesProvider = value; } + } + + protected virtual string MainJsSource => null; + protected virtual string NativeObjectName => null; + protected virtual string ModuleName => null; + protected virtual string Source => null; + + protected virtual object CreateNativeObject() => null; + + protected virtual string[] Events => new string[0]; + + protected virtual KeyValuePair[] PropertiesValues => new KeyValuePair[0]; + + string IViewModule.MainJsSource => MainJsSource; + + string IViewModule.NativeObjectName => NativeObjectName; + + string IViewModule.Name => ModuleName; + + string IViewModule.Source => Source; + + object IViewModule.CreateNativeObject() => CreateNativeObject(); + + string[] IViewModule.Events => Events; + + string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(MainJsSource); + + string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(MainJsSource); + + KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; + + void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { + frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; + frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); + this.frame = frame; + this.childViewHost = childViewHost; + } + + // ease access in generated code + protected IExecutionEngine ExecutionEngine { + get { + var engine = frame.ExecutionEngine; + if (engine == null) { + throw new InvalidOperationException("View module must be bound to an execution engine"); + } + return engine; + } + } + + public event CustomResourceRequestedEventHandler CustomResourceRequested { + add => frame.CustomResourceRequestedHandler += value; + remove => frame.CustomResourceRequestedHandler -= value; + } + + public T WithPlugin() { + return frame.GetPlugin(); + } + + public void Load() { + childViewHost?.LoadComponent(frame.Name, this); + } + + public T GetOrAddChildView(string frameName) where T : IViewModule, new() { + if (childViewHost == null) { + return default(T); + } + return childViewHost.GetOrAddChildView(FormatChildViewName(frameName)); + } + + public bool AddChildView(IViewModule childView, string frameName) { + if (childViewHost == null) { + return false; + } + return childViewHost.AddChildView(childView, FormatChildViewName(frameName)); + } + + public ReactView Host => childViewHost?.Host; + + private string FormatChildViewName(string name) => frame.Name + (string.IsNullOrEmpty(frame.Name) ? "" : ".") + name; + } +} \ No newline at end of file From e15e56305db83df8695a90a95d805e562cb7b5db Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Tue, 31 Oct 2023 19:34:30 +0000 Subject: [PATCH 09/33] more changes to hot reload --- .../FileDependenciesProvider.cs | 4 +-- ReactViewControl/IChildViewHost.cs | 2 -- ReactViewControl/ReactView.cs | 2 +- ReactViewControl/ReactViewFactory.cs | 5 ---- ReactViewControl/ReactViewRender.cs | 27 +++++++------------ ReactViewControl/ViewModuleContainer.cs | 10 +++---- Sample.Avalonia/ExtendedReactView.cs | 9 ++++--- Sample.Avalonia/ExtendedReactViewFactory.cs | 6 ++--- .../WebpackDevDependenciesProvider.cs | 5 +--- 9 files changed, 26 insertions(+), 44 deletions(-) diff --git a/ReactViewControl.Avalonia/FileDependenciesProvider.cs b/ReactViewControl.Avalonia/FileDependenciesProvider.cs index ad546507..85fa69d7 100644 --- a/ReactViewControl.Avalonia/FileDependenciesProvider.cs +++ b/ReactViewControl.Avalonia/FileDependenciesProvider.cs @@ -17,9 +17,9 @@ public FileDependenciesProvider(string sourcePath) { this.sourcePath = sourcePath; } - public string[] GetCssDependencies(string filename) => DependencyJsSourcesCache.Value; + public string[] GetCssDependencies(string filename) => CssSourcesCache.Value; - public string[] GetJsDependencies(string filename) => CssSourcesCache.Value; + public string[] GetJsDependencies(string filename) => DependencyJsSourcesCache.Value; private Lazy DependencyJsSourcesCache { get; } private Lazy CssSourcesCache { get; } diff --git a/ReactViewControl/IChildViewHost.cs b/ReactViewControl/IChildViewHost.cs index 793127fe..8f10eccb 100644 --- a/ReactViewControl/IChildViewHost.cs +++ b/ReactViewControl/IChildViewHost.cs @@ -8,7 +8,5 @@ public interface IChildViewHost { bool AddChildView(IViewModule childView, string frameName); ReactView Host { get; } - - bool IsHotReloadEnabled { get; } } } diff --git a/ReactViewControl/ReactView.cs b/ReactViewControl/ReactView.cs index cedce918..4ccc1ee9 100644 --- a/ReactViewControl/ReactView.cs +++ b/ReactViewControl/ReactView.cs @@ -19,7 +19,7 @@ public abstract partial class ReactView : IDisposable { private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { ReactViewRender InnerCreateView() { - var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.DevServerURI, factory.ModuleDependenciesProvider); + var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.ModuleDependenciesProvider); if (factory.ShowDeveloperTools) { view.ShowDeveloperTools(); } diff --git a/ReactViewControl/ReactViewFactory.cs b/ReactViewControl/ReactViewFactory.cs index defc2f92..a3bc43c2 100644 --- a/ReactViewControl/ReactViewFactory.cs +++ b/ReactViewControl/ReactViewFactory.cs @@ -31,11 +31,6 @@ public class ReactViewFactory { /// public virtual bool EnableViewPreload => true; - /// - /// Webpack dev server url. Setting this value will enable hot reload. eg: new Uri("http://localhost:8080") - /// - public virtual Uri DevServerURI => null; - /// /// Module dependencies provider. /// diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index a3198557..873b3202 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -36,7 +36,7 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable { private ResourceUrl defaultStyleSheet; private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) - public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, Uri devServerUri = null, IModuleDependenciesProvider moduleDependenciesProvider = null) { + public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, IModuleDependenciesProvider moduleDependenciesProvider = null) { UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; // must useSharedDomain for the local storage to be shared @@ -53,8 +53,10 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func initia DefaultStyleSheet = defaultStyleSheet; PluginsFactory = initializePlugins; EnableDebugMode = enableDebugMode; - DevServerUri = devServerUri; - ModuleDependenciesProvider = moduleDependenciesProvider; + + if (moduleDependenciesProvider != null) { + ModuleDependenciesProvider = moduleDependenciesProvider; + } GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame @@ -91,11 +93,6 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func initia public ReactView Host { get; set; } - /// - /// True when hot reload is enabled. - /// - public bool IsHotReloadEnabled => DevServerUri != null; - public bool IsDisposing => WebView.IsDisposing; /// @@ -126,14 +123,8 @@ public bool EnableDebugMode { } } - /// - /// Gets webpack dev server url. - /// - public Uri DevServerUri { get; } - public IModuleDependenciesProvider ModuleDependenciesProvider { get; } - /// /// Gets or sets the webview zoom percentage (1 = 100%) /// @@ -563,11 +554,11 @@ private string ToFullUrl(string url) { if (url.OrdinalContains(Uri.SchemeDelimiter)) { return url; } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { - if (IsHotReloadEnabled) { - return new Uri(DevServerUri, url).ToString(); - } else { + // if (IsHotReloadEnabled) { + // return new Uri(DevServerUri, url).ToString(); + // } else { return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); - } + // } } else { return new ResourceUrl(UserCallingAssembly, url).ToString(); } diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 019a41ca..81ed267b 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -4,21 +4,21 @@ namespace ReactViewControl { public abstract class ViewModuleContainer : IViewModule { - - private const string JsEntryFileExtension = ".js.entry"; - private const string CssEntryFileExtension = ".css.entry"; - private IFrame frame; private IChildViewHost childViewHost; private IModuleDependenciesProvider dependenciesProvider; public ViewModuleContainer() { frame = new FrameInfo("dummy"); + dependenciesProvider = new FileDependenciesProvider(MainJsSource); } public virtual IModuleDependenciesProvider DependenciesProvider { get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } - set { dependenciesProvider = value; } + set { if(value != null) { + dependenciesProvider = value; + } + } } protected virtual string MainJsSource => null; diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index 62c082b5..f50f6dc8 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -9,8 +9,11 @@ public abstract class ExtendedReactView : ReactView { public ExtendedReactView(IViewModule mainModule) : base(mainModule) { Settings.ThemeChanged += OnStylePreferenceChanged; - EmbeddedResourceRequested += OnEmbeddedResourceRequested; - mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; + + if (Factory.ModuleDependenciesProvider != null) { + mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; + EmbeddedResourceRequested += OnEmbeddedResourceRequested; + } } protected override void InnerDispose() { @@ -30,7 +33,7 @@ private void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resource } resourceUrl = new Uri(resourceUrl).PathAndQuery; - var devServerHost = new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); + var devServerHost = new Uri("http://localhost:8080/"); //new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); resourceHandler.Redirect(new Uri(devServerHost, resourceUrl).ToString()); } } diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 836c43af..41663edd 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -13,8 +13,8 @@ internal class ExtendedReactViewFactory : ReactViewFactory { public override IViewModule[] InitializePlugins() { var viewPlugin = new ViewPlugin(); #if DEBUG - if (DevServerURI != null) { - viewPlugin.DependenciesProvider = ModuleDependenciesProvider; + if (provider != null) { + viewPlugin.DependenciesProvider = provider; } #endif return new[]{ @@ -29,8 +29,6 @@ public override IViewModule[] InitializePlugins() { #if DEBUG public override bool EnableDebugMode => true; - public override Uri DevServerURI => new Uri("http://localhost:8080/Sample.Avalonia/"); - public override IModuleDependenciesProvider ModuleDependenciesProvider => provider; #endif diff --git a/Sample.Avalonia/WebpackDevDependenciesProvider.cs b/Sample.Avalonia/WebpackDevDependenciesProvider.cs index 7da81ffc..1a132cbf 100644 --- a/Sample.Avalonia/WebpackDevDependenciesProvider.cs +++ b/Sample.Avalonia/WebpackDevDependenciesProvider.cs @@ -1,5 +1,4 @@ using System.Net.Http; -using System.Reflection; using System.Text.Json; using ReactViewControl; @@ -9,8 +8,6 @@ namespace Sample.Avalonia; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -//using Newtonsoft.Json; public class WebPackDependenciesProvider : IModuleDependenciesProvider { @@ -73,7 +70,7 @@ public void RefreshDependencies() { if (shouldRefresh) { using var httpClient = new HttpClient(); var assembly = typeof(Program).Assembly.GetName().Name; - // var json = httpClient.GetStringAsync(new Uri(uri, $"{assembly}/{ManifestPath}")); + // var json = httpClient.GetStringAsync(new Uri(uri, $"{assembly}/{ManifestPath}")); var json = httpClient.GetStringAsync(new Uri(uri, ManifestPath)); json.Wait(); From 92f0af0975f6b9b56d5b3daa3ca30e56ca7cb658 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Wed, 1 Nov 2023 13:24:25 +0000 Subject: [PATCH 10/33] Code cleanup --- ReactViewControl/IViewModule.cs | 2 +- ReactViewControl/ReactView.cs | 496 +++++++++--------- ReactViewControl/ReactViewRender.cs | 15 +- ReactViewControl/ViewModuleContainer.cs | 5 +- Sample.Avalonia/TaskListView/TaskListView.tsx | 3 +- 5 files changed, 259 insertions(+), 262 deletions(-) diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index c4b2f92b..4691f68c 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -4,7 +4,7 @@ namespace ReactViewControl { public interface IViewModule { - public string MainJsSource { get; } + string MainJsSource { get; } IModuleDependenciesProvider DependenciesProvider{ get; set; } diff --git a/ReactViewControl/ReactView.cs b/ReactViewControl/ReactView.cs index 4ccc1ee9..b5b0dfaf 100644 --- a/ReactViewControl/ReactView.cs +++ b/ReactViewControl/ReactView.cs @@ -1,249 +1,249 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using WebViewControl; - -namespace ReactViewControl { - - public delegate void ResourceRequestedEventHandler(ResourceHandler resourceHandler); - - public delegate Resource CustomResourceRequestedEventHandler(string resourceKey, params string[] options); - - public abstract partial class ReactView : IDisposable { - - private static Dictionary CachedViews { get; } = new Dictionary(); - - private ReactViewRender View { get; } - - private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { - ReactViewRender InnerCreateView() { - var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.ModuleDependenciesProvider); - if (factory.ShowDeveloperTools) { - view.ShowDeveloperTools(); - } - return view; - } - - if (factory.EnableViewPreload) { - var factoryType = factory.GetType(); - // check if we have a view cached for the current factory - if (CachedViews.TryGetValue(factoryType, out var cachedView)) { - CachedViews.Remove(factoryType); - } - - // create a new view in the background and put it in the cache - AsyncExecuteInUI(() => { - if (!CachedViews.ContainsKey(factoryType)) { - CachedViews.Add(factoryType, InnerCreateView()); - } - }, lowPriority: true); - - if (cachedView != null) { - return cachedView; - } - } - - return InnerCreateView(); - } - - protected ReactView(IViewModule mainModule) { - View = CreateReactViewInstance(Factory); - - View.Host = this; - MainModule = mainModule; - // bind main module (this is needed so that the plugins are available right away) - View.BindComponent(mainModule); - - ExtraInitialize(); - } - - partial void ExtraInitialize(); - - ~ReactView() { - Dispose(); - } - - public void Dispose() { - InnerDispose(); - View?.Dispose(); - GC.SuppressFinalize(this); - } - - protected virtual void InnerDispose() { } - - /// - /// Factory used to configure the initial properties of the control. - /// - protected virtual ReactViewFactory Factory => new ReactViewFactory(); - - protected void RefreshDefaultStyleSheet() { - View.DefaultStyleSheet = Factory.DefaultStyleSheet; - AsyncExecuteInUI(() => CachedViews.Remove(Factory.GetType()), lowPriority: true); - } - - /// - /// Tries to loads the main component. - /// - protected void TryLoadComponent() { - TryLoadComponent(true); - } - - /// - /// Tries to loads the main component. - /// - /// Tries to initialize underlying view if wasn't yet. - private void TryLoadComponent(bool ensureViewInitialized) { - if (View.IsMainComponentLoaded) { - return; - } - - if (ensureViewInitialized) { - // we're performing an explicit load and view has not been initialized - // try initializing it - AsyncExecuteInUI(() => View.EnsureInitialized(), lowPriority: false); - } - - View.LoadComponent(MainModule); - } - - /// - /// Retrieves the specified plugin module instance. - /// - /// Type of the plugin to retrieve. - /// - public T WithPlugin() { - return View.WithPlugin(); - } - - /// - /// Enables or disables debug mode. - /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details - /// when a resource fails to load. - /// - public bool EnableDebugMode { get => View.EnableDebugMode; set => View.EnableDebugMode = value; } - - /// - /// True when the main component has been rendered. - /// - public bool IsReady => View.IsReady; - - /// - /// Gets or sets the control zoom percentage (1 = 100%) - /// - public double ZoomPercentage { get => View.ZoomPercentage; set => View.ZoomPercentage = value; } - - /// - /// Event fired when the component is rendered and ready for interaction. - /// - public event Action Ready { - add { View.Ready += value; } - remove { View.Ready -= value; } - } - - /// - /// Event fired when an async exception occurs (eg: executing javascript) - /// - public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { - add { View.UnhandledAsyncException += value; } - remove { View.UnhandledAsyncException -= value; } - } - - /// - /// Event fired when a resource fails to load. - /// - public event ResourceLoadFailedEventHandler ResourceLoadFailed { - add { View.ResourceLoadFailed += value; } - remove { View.ResourceLoadFailed -= value; } - } - - /// - /// Handle embedded resource requests. You can use this event to change the resource being loaded. - /// - public event ResourceRequestedEventHandler EmbeddedResourceRequested { - add { View.EmbeddedResourceRequested += value; } - remove { View.EmbeddedResourceRequested -= value; } - } - - /// - /// Handle custom resource requests. Use this event to load the resource based on the provided key. - /// This handler will be called before the frame handler. - /// - public event CustomResourceRequestedEventHandler CustomResourceRequested { - add { View.CustomResourceRequested += value; } - remove { View.CustomResourceRequested -= value; } - } - - /// - /// Handle external resource requests. - /// Call to handle the request in an async way. - /// - public event ResourceRequestedEventHandler ExternalResourceRequested { - add { View.ExternalResourceRequested += value; } - remove { View.ExternalResourceRequested -= value; } - } - - /// - /// Handle drag of files. Use this event to get the full path of the files being dragged. - /// - public event FilesDraggingEventHandler FilesDragging { - add { View.FilesDragging += value; } - remove { View.FilesDragging -= value; } - } - - /// - /// Handle drag of text. Use this event to get the text content being dragged. - /// - public event TextDraggingEventHandler TextDragging { - add { View.TextDragging += value; } - remove { View.TextDragging -= value; } - } - - /// - /// Opens the developer tools. - /// - public void ShowDeveloperTools() { - View.ShowDeveloperTools(); - } - - /// - /// Closes the developer tools. - /// - public void CloseDeveloperTools() { - View.CloseDeveloperTools(); - } - - /// - /// View module of this control. - /// - protected IViewModule MainModule { get; } - - /// - /// Number of preloaded views that are mantained in cache for each view. - /// Components with different property values are stored in different cache entries. - /// Defaults to 6. - /// - public static int PreloadedCacheEntriesSize { get; set; } = 6; - - /// - /// Gets the edition commands. - /// - public EditCommands EditCommands => View.EditCommands; - - /// - /// Called when executing a native method. - /// - protected virtual Task OnNativeMethodCalled(Func nativeMethod) => Task.FromResult(nativeMethod()); - - /// - /// Called before executing/evaluating a JS method - /// - protected virtual void OnBeforeExecuteMethod() { } - - internal void HandledBeforeExecuteMethod() => OnBeforeExecuteMethod(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Task CallNativeMethod(Func nativeMethod) => OnNativeMethodCalled(nativeMethod); - } +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using WebViewControl; + +namespace ReactViewControl { + + public delegate void ResourceRequestedEventHandler(ResourceHandler resourceHandler); + + public delegate Resource CustomResourceRequestedEventHandler(string resourceKey, params string[] options); + + public abstract partial class ReactView : IDisposable { + + private static Dictionary CachedViews { get; } = new Dictionary(); + + private ReactViewRender View { get; } + + private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { + ReactViewRender InnerCreateView() { + var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.ModuleDependenciesProvider); + if (factory.ShowDeveloperTools) { + view.ShowDeveloperTools(); + } + return view; + } + + if (factory.EnableViewPreload) { + var factoryType = factory.GetType(); + // check if we have a view cached for the current factory + if (CachedViews.TryGetValue(factoryType, out var cachedView)) { + CachedViews.Remove(factoryType); + } + + // create a new view in the background and put it in the cache + AsyncExecuteInUI(() => { + if (!CachedViews.ContainsKey(factoryType)) { + CachedViews.Add(factoryType, InnerCreateView()); + } + }, lowPriority: true); + + if (cachedView != null) { + return cachedView; + } + } + + return InnerCreateView(); + } + + protected ReactView(IViewModule mainModule) { + View = CreateReactViewInstance(Factory); + + View.Host = this; + MainModule = mainModule; + // bind main module (this is needed so that the plugins are available right away) + View.BindComponent(mainModule); + + ExtraInitialize(); + } + + partial void ExtraInitialize(); + + ~ReactView() { + Dispose(); + } + + public void Dispose() { + InnerDispose(); + View?.Dispose(); + GC.SuppressFinalize(this); + } + + protected virtual void InnerDispose() { } + + /// + /// Factory used to configure the initial properties of the control. + /// + protected virtual ReactViewFactory Factory => new ReactViewFactory(); + + protected void RefreshDefaultStyleSheet() { + View.DefaultStyleSheet = Factory.DefaultStyleSheet; + AsyncExecuteInUI(() => CachedViews.Remove(Factory.GetType()), lowPriority: true); + } + + /// + /// Tries to loads the main component. + /// + protected void TryLoadComponent() { + TryLoadComponent(true); + } + + /// + /// Tries to loads the main component. + /// + /// Tries to initialize underlying view if wasn't yet. + private void TryLoadComponent(bool ensureViewInitialized) { + if (View.IsMainComponentLoaded) { + return; + } + + if (ensureViewInitialized) { + // we're performing an explicit load and view has not been initialized + // try initializing it + AsyncExecuteInUI(() => View.EnsureInitialized(), lowPriority: false); + } + + View.LoadComponent(MainModule); + } + + /// + /// Retrieves the specified plugin module instance. + /// + /// Type of the plugin to retrieve. + /// + public T WithPlugin() { + return View.WithPlugin(); + } + + /// + /// Enables or disables debug mode. + /// In debug mode the webview developer tools becomes available pressing F12 and the webview shows an error message at the top with the error details + /// when a resource fails to load. + /// + public bool EnableDebugMode { get => View.EnableDebugMode; set => View.EnableDebugMode = value; } + + /// + /// True when the main component has been rendered. + /// + public bool IsReady => View.IsReady; + + /// + /// Gets or sets the control zoom percentage (1 = 100%) + /// + public double ZoomPercentage { get => View.ZoomPercentage; set => View.ZoomPercentage = value; } + + /// + /// Event fired when the component is rendered and ready for interaction. + /// + public event Action Ready { + add { View.Ready += value; } + remove { View.Ready -= value; } + } + + /// + /// Event fired when an async exception occurs (eg: executing javascript) + /// + public event UnhandledAsyncExceptionEventHandler UnhandledAsyncException { + add { View.UnhandledAsyncException += value; } + remove { View.UnhandledAsyncException -= value; } + } + + /// + /// Event fired when a resource fails to load. + /// + public event ResourceLoadFailedEventHandler ResourceLoadFailed { + add { View.ResourceLoadFailed += value; } + remove { View.ResourceLoadFailed -= value; } + } + + /// + /// Handle embedded resource requests. You can use this event to change the resource being loaded. + /// + public event ResourceRequestedEventHandler EmbeddedResourceRequested { + add { View.EmbeddedResourceRequested += value; } + remove { View.EmbeddedResourceRequested -= value; } + } + + /// + /// Handle custom resource requests. Use this event to load the resource based on the provided key. + /// This handler will be called before the frame handler. + /// + public event CustomResourceRequestedEventHandler CustomResourceRequested { + add { View.CustomResourceRequested += value; } + remove { View.CustomResourceRequested -= value; } + } + + /// + /// Handle external resource requests. + /// Call to handle the request in an async way. + /// + public event ResourceRequestedEventHandler ExternalResourceRequested { + add { View.ExternalResourceRequested += value; } + remove { View.ExternalResourceRequested -= value; } + } + + /// + /// Handle drag of files. Use this event to get the full path of the files being dragged. + /// + public event FilesDraggingEventHandler FilesDragging { + add { View.FilesDragging += value; } + remove { View.FilesDragging -= value; } + } + + /// + /// Handle drag of text. Use this event to get the text content being dragged. + /// + public event TextDraggingEventHandler TextDragging { + add { View.TextDragging += value; } + remove { View.TextDragging -= value; } + } + + /// + /// Opens the developer tools. + /// + public void ShowDeveloperTools() { + View.ShowDeveloperTools(); + } + + /// + /// Closes the developer tools. + /// + public void CloseDeveloperTools() { + View.CloseDeveloperTools(); + } + + /// + /// View module of this control. + /// + protected IViewModule MainModule { get; } + + /// + /// Number of preloaded views that are mantained in cache for each view. + /// Components with different property values are stored in different cache entries. + /// Defaults to 6. + /// + public static int PreloadedCacheEntriesSize { get; set; } = 6; + + /// + /// Gets the edition commands. + /// + public EditCommands EditCommands => View.EditCommands; + + /// + /// Called when executing a native method. + /// + protected virtual Task OnNativeMethodCalled(Func nativeMethod) => Task.FromResult(nativeMethod()); + + /// + /// Called before executing/evaluating a JS method + /// + protected virtual void OnBeforeExecuteMethod() { } + + internal void HandledBeforeExecuteMethod() => OnBeforeExecuteMethod(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Task CallNativeMethod(Func nativeMethod) => OnNativeMethodCalled(nativeMethod); + } } \ No newline at end of file diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 873b3202..84460a1c 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -550,18 +550,15 @@ private void UnregisterNativeObject(IViewModule module, FrameInfo frame) { /// /// private string ToFullUrl(string url) { - // TODO: BUG AQUI if (url.OrdinalContains(Uri.SchemeDelimiter)) { return url; - } else if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { - // if (IsHotReloadEnabled) { - // return new Uri(DevServerUri, url).ToString(); - // } else { - return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); - // } - } else { - return new ResourceUrl(UserCallingAssembly, url).ToString(); } + + if (url.OrdinalStartsWith(ResourceUrl.PathSeparator)) { + return new ResourceUrl(ResourceUrl.EmbeddedScheme, url).ToString(); + } + + return new ResourceUrl(UserCallingAssembly, url).ToString(); } /// diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 81ed267b..b7c32408 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -8,14 +8,15 @@ public abstract class ViewModuleContainer : IViewModule { private IChildViewHost childViewHost; private IModuleDependenciesProvider dependenciesProvider; - public ViewModuleContainer() { + protected ViewModuleContainer() { frame = new FrameInfo("dummy"); dependenciesProvider = new FileDependenciesProvider(MainJsSource); } public virtual IModuleDependenciesProvider DependenciesProvider { get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } - set { if(value != null) { + set { + if(value != null) { dependenciesProvider = value; } } diff --git a/Sample.Avalonia/TaskListView/TaskListView.tsx b/Sample.Avalonia/TaskListView/TaskListView.tsx index d2496b98..341f602c 100644 --- a/Sample.Avalonia/TaskListView/TaskListView.tsx +++ b/Sample.Avalonia/TaskListView/TaskListView.tsx @@ -41,7 +41,6 @@ class TaskListItem extends React.Component<{ task: ITask }, {}, IPluginsContext>
{this.props.task.text}
-
Added by: @@ -99,7 +98,7 @@ export default class TaskListView extends React.Component xau +
{this.renderItems()}
); From ba0e3b40f48cc232e4d46b21584902bc469f3a81 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Wed, 1 Nov 2023 13:39:15 +0000 Subject: [PATCH 11/33] webpack config --- ViewGenerator/tools/webpack/webpack_views.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ViewGenerator/tools/webpack/webpack_views.config.ts b/ViewGenerator/tools/webpack/webpack_views.config.ts index 0e496159..5b398955 100644 --- a/ViewGenerator/tools/webpack/webpack_views.config.ts +++ b/ViewGenerator/tools/webpack/webpack_views.config.ts @@ -35,7 +35,7 @@ const config = (env) => { throw new Error("Extended configuration file not found."); } }; - + const sanitizedPluginsRelativePath: string = sanitizeCommandLineParam(env.pluginsRelativePath); const standardConfig: Configuration = getCommonConfiguration(env.useCache ? "viewsCache" : "", "Views", sanitizeCommandLineParam(env.assemblyName), sanitizedPluginsRelativePath, env.forHotReload); @@ -88,7 +88,7 @@ const config = (env) => { if (env.forHotReload) { standardConfig.output.hotUpdateChunkFilename = OutputDirectoryDefault + IdPlaceholder + "." + FullHashPlaceholder + ".hot-update.js"; standardConfig.output.hotUpdateMainFilename = OutputDirectoryDefault + RuntimePlaceholder + "." + FullHashPlaceholder + ".hot-update.json"; - + // @ts-ignore standardConfig.devServer = { client: { From 6c165c401e4130f8c72fb697cd755f192f69bd48 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Wed, 1 Nov 2023 13:41:35 +0000 Subject: [PATCH 12/33] remove file --- Sample.Avalonia/rundevserver.sh | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sample.Avalonia/rundevserver.sh diff --git a/Sample.Avalonia/rundevserver.sh b/Sample.Avalonia/rundevserver.sh deleted file mode 100644 index f0fb2ea5..00000000 --- a/Sample.Avalonia/rundevserver.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash -VIEW_GENERATOR_PATH=/Users/mpm/Development/git/O11IDE/packages/viewgenerator/1.1.17 -VIEW_PACKER_PATH=/Users/mpm/Development/git/O11IDE/packages/viewpacker/1.1.5 -PLUGINS_FOLDER=$~dp0../Plugins -PLUGIN_APPLICATION_CREATION_WIZARD=$PLUGINS_FOLDER/ApplicationCreationWizard/ServiceStudio -PLUGIN_CHROME_DEBUGGER_ADAPTER=$PLUGINS_FOLDER/ChromeDebuggerAdapter/ServiceStudio -FRAMEWORK_OUSYSTEMS_NODE_MODULES=/Users/mpm/Development/git/O11IDE/ServiceStudio/ServiceStudio.WebViewImplementation/../ServiceStudio.WebViewImplementation.Framework/node_modules/@outsystems -FRAMEWORK_OUSYSTEMS_DESIGN_SYSTEM=/Users/mpm/Development/git/O11IDE/ServiceStudio/ServiceStudio.WebViewImplementation/../ServiceStudio.WebViewImplementation.Framework/node_modules/@os-designsystem - -node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=true --env pluginsRelativePath='..' --env assemblyName="Sample.Avalonia" - -node "/Users/mpm/Development/git/ReactView/packages/viewpacker/1.1.5/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="/Users/mpm/Development/git/ReactView/packages/viewgenerator/1.1.17/tools/webpack/webpack_views.config.js" --env forHotReload=true --mode=development --devtool=inline-source-map --env useCache=true --env pluginsRelativePath='' --env assemblyName='Sample.Avalonia' \ No newline at end of file From e1db8a5e1e12e23a4eca91cfbc65974dc32599e4 Mon Sep 17 00:00:00 2001 From: Miguel Marques Date: Thu, 2 Nov 2023 14:25:46 +0000 Subject: [PATCH 13/33] Applies code review suggestions --- .../FileDependenciesProvider.cs | 4 +- ReactViewControl/IViewModule.cs | 2 +- ReactViewControl/ReactViewRender.cs | 5 +- ReactViewControl/ViewModuleContainer.cs | 3 +- ViewGeneratorCore/tools/package-lock.json | 170 +----------------- 5 files changed, 6 insertions(+), 178 deletions(-) diff --git a/ReactViewControl.Avalonia/FileDependenciesProvider.cs b/ReactViewControl.Avalonia/FileDependenciesProvider.cs index 85fa69d7..98998481 100644 --- a/ReactViewControl.Avalonia/FileDependenciesProvider.cs +++ b/ReactViewControl.Avalonia/FileDependenciesProvider.cs @@ -4,7 +4,7 @@ using WebViewControl; namespace ReactViewControl { - class FileDependenciesProvider : IModuleDependenciesProvider { + internal class FileDependenciesProvider : IModuleDependenciesProvider { private const string JsEntryFileExtension = ".js.entry"; private const string CssEntryFileExtension = ".css.entry"; @@ -33,7 +33,7 @@ private string[] GetDependenciesFromEntriesFile(string extension) { using (var reader = new StreamReader(stream)) { var allEntries = reader.ReadToEnd(); if (allEntries != null && allEntries != string.Empty) { - return allEntries.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + return allEntries.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index 4691f68c..1334dfc9 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -6,7 +6,7 @@ public interface IViewModule { string MainJsSource { get; } - IModuleDependenciesProvider DependenciesProvider{ get; set; } + IModuleDependenciesProvider DependenciesProvider { get; set; } string[] DependencyJsSources { get; } diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 84460a1c..24dfa754 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -53,10 +53,7 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func initia DefaultStyleSheet = defaultStyleSheet; PluginsFactory = initializePlugins; EnableDebugMode = enableDebugMode; - - if (moduleDependenciesProvider != null) { - ModuleDependenciesProvider = moduleDependenciesProvider; - } + ModuleDependenciesProvider = moduleDependenciesProvider; GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index b7c32408..7bcbe5f0 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -10,13 +10,12 @@ public abstract class ViewModuleContainer : IViewModule { protected ViewModuleContainer() { frame = new FrameInfo("dummy"); - dependenciesProvider = new FileDependenciesProvider(MainJsSource); } public virtual IModuleDependenciesProvider DependenciesProvider { get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } set { - if(value != null) { + if (value != null) { dependenciesProvider = value; } } diff --git a/ViewGeneratorCore/tools/package-lock.json b/ViewGeneratorCore/tools/package-lock.json index 8f353610..d4452c1e 100644 --- a/ViewGeneratorCore/tools/package-lock.json +++ b/ViewGeneratorCore/tools/package-lock.json @@ -1,174 +1,6 @@ { - "name": "tools", - "lockfileVersion": 2, "requires": true, - "packages": { - "": { - "license": "ISC", - "dependencies": { - "@types/node": "^12.6.8" - }, - "devDependencies": { - "@outsystems/ts2lang": "1.0.21" - } - }, - "node_modules/@outsystems/ts2lang": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@outsystems/ts2lang/-/ts2lang-1.0.21.tgz", - "integrity": "sha512-K4v/hOvImzm/28ZpLZmiK3CRzdWm0SMJEN3mMbgKbj2rDAzLuAfE53RNE5DLAGoZGMeCrLkq6yuT9wLrAHANog==", - "dev": true, - "dependencies": { - "@outsystems/ts2lang": "^1.0.12", - "commander": "^2.9.0", - "glob": "^7.1.2", - "is-directory": "^0.3.1", - "merge": "^1.2.0", - "typescript": "3.4" - }, - "bin": { - "ts2lang": "ts2lang-main.js" - } - }, - "node_modules/@types/node": { - "version": "12.20.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.4.tgz", - "integrity": "sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ==" - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - }, + "lockfileVersion": 1, "dependencies": { "@outsystems/ts2lang": { "version": "1.0.21", From 0102b7a463300e0e788582d3077393a290ac899d Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Thu, 2 Nov 2023 10:19:11 +0000 Subject: [PATCH 14/33] Bump versions --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 05cce300..5086d768 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ x64;ARM64 2.0.0.0 2.0.0.0 - 2.117.1 + 2.117.2 OutSystems ReactView Copyright © OutSystems 2023 From 4fce4459d514ca44cf6938da6ab35456116a94a3 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Thu, 2 Nov 2023 17:33:23 +0000 Subject: [PATCH 15/33] Bump ViewGenerator --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0217ee41..c9236194 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 6d813139860e5cc02c9153a93a8071515b0f308b Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 10:42:28 +0000 Subject: [PATCH 16/33] Bump ViewGenerator --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c9236194..68c64770 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 6d6b3907d69de4e13a2a102aa52ca5d29f58d75c Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 12:12:33 +0000 Subject: [PATCH 17/33] Make dependencies provider readonly --- ReactViewControl/IViewModule.cs | 4 ++-- ReactViewControl/ReactViewRender.cs | 8 ++------ ReactViewControl/ViewModuleContainer.cs | 8 ++------ Sample.Avalonia/ExtendedReactView.cs | 1 - Sample.Avalonia/ExtendedReactViewFactory.cs | 5 ----- 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index 1334dfc9..0960281c 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -6,7 +6,7 @@ public interface IViewModule { string MainJsSource { get; } - IModuleDependenciesProvider DependenciesProvider { get; set; } + IModuleDependenciesProvider DependenciesProvider { get; } string[] DependencyJsSources { get; } @@ -24,7 +24,7 @@ public interface IViewModule { KeyValuePair[] PropertiesValues { get; } - void Bind(IFrame frame, IChildViewHost host = null); + void Bind(IFrame frame, IChildViewHost host = null, IModuleDependenciesProvider moduleDependenciesProvider = null); event CustomResourceRequestedEventHandler CustomResourceRequested; diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 24dfa754..330d399d 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -271,10 +271,6 @@ private void TryLoadComponent(FrameInfo frame) { RegisterNativeObject(frame.Component, frame); - if (ModuleDependenciesProvider != null) { - frame.Component.DependenciesProvider = ModuleDependenciesProvider; - } - Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0); if (isInputDisabled && frame.IsMain) { Loader.DisableMouseInteractions(); @@ -308,7 +304,7 @@ private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); foreach (var plugin in plugins) { - plugin.Bind(frame); + plugin.Bind(frame, moduleDependenciesProvider: ModuleDependenciesProvider); } } @@ -409,7 +405,7 @@ public bool AddChildView(IViewModule childView, string frameName) { /// private void BindComponentToFrame(IViewModule component, FrameInfo frame) { frame.Component = component; - component.Bind(frame, this); + component.Bind(frame, this, ModuleDependenciesProvider); } /// diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 7bcbe5f0..44e84e7c 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -14,11 +14,6 @@ protected ViewModuleContainer() { public virtual IModuleDependenciesProvider DependenciesProvider { get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } - set { - if (value != null) { - dependenciesProvider = value; - } - } } protected virtual string MainJsSource => null; @@ -50,11 +45,12 @@ public virtual IModuleDependenciesProvider DependenciesProvider { KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; - void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { + void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost, IModuleDependenciesProvider moduleDependenciesProvider) { frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); this.frame = frame; this.childViewHost = childViewHost; + this.dependenciesProvider = moduleDependenciesProvider; } // ease access in generated code diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index f50f6dc8..b2a2b326 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -11,7 +11,6 @@ public ExtendedReactView(IViewModule mainModule) : base(mainModule) { Settings.ThemeChanged += OnStylePreferenceChanged; if (Factory.ModuleDependenciesProvider != null) { - mainModule.DependenciesProvider = Factory.ModuleDependenciesProvider; EmbeddedResourceRequested += OnEmbeddedResourceRequested; } } diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 41663edd..560e1a5f 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -12,11 +12,6 @@ internal class ExtendedReactViewFactory : ReactViewFactory { public override IViewModule[] InitializePlugins() { var viewPlugin = new ViewPlugin(); -#if DEBUG - if (provider != null) { - viewPlugin.DependenciesProvider = provider; - } -#endif return new[]{ viewPlugin }; From 713802d4559f8b3e62d309ee0cad1b81a26b9567 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 12:36:51 +0000 Subject: [PATCH 18/33] Fixed example --- Sample.Avalonia/MainView/MainView.scss | 1 + Sample.Avalonia/MainView/MainView.tsx | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Sample.Avalonia/MainView/MainView.scss b/Sample.Avalonia/MainView/MainView.scss index 5e401336..c77b3418 100644 --- a/Sample.Avalonia/MainView/MainView.scss +++ b/Sample.Avalonia/MainView/MainView.scss @@ -9,6 +9,7 @@ body { background-position: center; background-size: contain; overflow-y: hidden; + background-image: url("./Tasks.png"); } .title { diff --git a/Sample.Avalonia/MainView/MainView.tsx b/Sample.Avalonia/MainView/MainView.tsx index bd868cab..56757c2b 100644 --- a/Sample.Avalonia/MainView/MainView.tsx +++ b/Sample.Avalonia/MainView/MainView.tsx @@ -4,8 +4,6 @@ import ViewPlugin from "./../ViewPlugin/ViewPlugin"; import { IPluginsContext } from "PluginsProvider"; import "./MainView.scss"; // import a stylesheet import TaskListView from "./../TaskListView/TaskListView"; // import another component -import * as Styles from "./MainView.scss"; // import variables from SASS -//import * as BackgroundImage from "./Tasks.png"; // import images export interface ITaskCreationDetails { text: string; @@ -75,14 +73,6 @@ export default class MainView extends React.Component { From 367f442ea52c235b73f79382009f9c310319dddb Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 12:59:46 +0000 Subject: [PATCH 19/33] Simplified example --- Sample.Avalonia/ExtendedReactView.cs | 40 ++++++++++------ Sample.Avalonia/ExtendedReactViewFactory.cs | 41 ++++++++++------ ...er.cs => HotReloadDependenciesProvider.cs} | 47 ++++++++----------- Sample.Avalonia/MainView/MainView.scss | 1 - 4 files changed, 71 insertions(+), 58 deletions(-) rename Sample.Avalonia/{WebpackDevDependenciesProvider.cs => HotReloadDependenciesProvider.cs} (62%) diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index b2a2b326..4e4f1159 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -3,16 +3,40 @@ namespace Sample.Avalonia { - public abstract class ExtendedReactView : ReactView { + public partial class ExtendedReactView : ReactView { + +#if DEBUG + private static readonly Uri DevServerUri; + + static ExtendedReactView() { + var uri = Environment.GetEnvironmentVariable("DEV_SERVER_URI"); + if (uri != null) { + DevServerUri = new Uri(uri); + } + } + + private static void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { + var resourceUrl = resourceHandler.Url; + if (resourceUrl.Contains("ReactViewResources")) { + return; + } + + resourceUrl = new Uri(resourceUrl).PathAndQuery; + resourceHandler.Redirect(new Uri(DevServerUri, resourceUrl).ToString()); + } +#endif + protected override ReactViewFactory Factory => new ExtendedReactViewFactory(); public ExtendedReactView(IViewModule mainModule) : base(mainModule) { Settings.ThemeChanged += OnStylePreferenceChanged; - if (Factory.ModuleDependenciesProvider != null) { +#if DEBUG + if (DevServerUri != null) { EmbeddedResourceRequested += OnEmbeddedResourceRequested; } +#endif } protected override void InnerDispose() { @@ -23,17 +47,5 @@ protected override void InnerDispose() { private void OnStylePreferenceChanged() { RefreshDefaultStyleSheet(); } - - private void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { - var resourceUrl = resourceHandler.Url; - - if (resourceUrl.Contains("ReactViewResources")) { - return; - } - - resourceUrl = new Uri(resourceUrl).PathAndQuery; - var devServerHost = new Uri("http://localhost:8080/"); //new Uri(Factory.DevServerURI.GetLeftPart(UriPartial.Authority)); - resourceHandler.Redirect(new Uri(devServerHost, resourceUrl).ToString()); - } } } \ No newline at end of file diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index 560e1a5f..baa90cac 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -4,28 +4,39 @@ namespace Sample.Avalonia { - internal class ExtendedReactViewFactory : ReactViewFactory { - private static WebPackDependenciesProvider provider = new WebPackDependenciesProvider(new Uri("http://localhost:8080/Sample.Avalonia/")); + partial class ExtendedReactView { + + protected class ExtendedReactViewFactory : ReactViewFactory { - public override ResourceUrl DefaultStyleSheet => - new ResourceUrl(typeof(ExtendedReactViewFactory).Assembly, "Generated", Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); + public override ResourceUrl DefaultStyleSheet => + new(typeof(ExtendedReactViewFactory).Assembly, "Generated", + Settings.IsLightTheme ? "LightTheme.css" : "DarkTheme.css"); - public override IViewModule[] InitializePlugins() { - var viewPlugin = new ViewPlugin(); - return new[]{ - viewPlugin - }; - } + public override IViewModule[] InitializePlugins() { + var viewPlugin = new ViewPlugin(); + return new IViewModule[] { viewPlugin }; + } - public override bool ShowDeveloperTools => false; + public override bool ShowDeveloperTools => false; - public override bool EnableViewPreload => true; + public override bool EnableViewPreload => true; #if DEBUG - public override bool EnableDebugMode => true; + public override bool EnableDebugMode => true; + + private static HotReloadDependenciesProvider hotReloadDependenciesProvider; - public override IModuleDependenciesProvider ModuleDependenciesProvider => - provider; + public override IModuleDependenciesProvider ModuleDependenciesProvider { + get { + if (DevServerUri != null) { + return hotReloadDependenciesProvider ??= new HotReloadDependenciesProvider( + new Uri($"{DevServerUri}{typeof(ExtendedReactViewFactory).Assembly.GetName().Name}/")); + } + + return base.ModuleDependenciesProvider; + } + } #endif + } } } \ No newline at end of file diff --git a/Sample.Avalonia/WebpackDevDependenciesProvider.cs b/Sample.Avalonia/HotReloadDependenciesProvider.cs similarity index 62% rename from Sample.Avalonia/WebpackDevDependenciesProvider.cs rename to Sample.Avalonia/HotReloadDependenciesProvider.cs index 1a132cbf..a18e2e20 100644 --- a/Sample.Avalonia/WebpackDevDependenciesProvider.cs +++ b/Sample.Avalonia/HotReloadDependenciesProvider.cs @@ -1,15 +1,14 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Http; using System.Text.Json; using ReactViewControl; namespace Sample.Avalonia; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -public class WebPackDependenciesProvider : IModuleDependenciesProvider { +public class HotReloadDependenciesProvider : IModuleDependenciesProvider { private Dictionary> dependencies; private readonly Uri uri; @@ -18,23 +17,17 @@ public class WebPackDependenciesProvider : IModuleDependenciesProvider { private DateTime lastRefresh; - public WebPackDependenciesProvider(Uri uri) { + public HotReloadDependenciesProvider(Uri uri) { this.uri = uri; basePath = GetBaseSegmentFromUri(); RefreshDependencies(); } - public string GetBaseSegmentFromUri() { - // TODO: fixme - return uri.ToString(); - //return "/" + uri.Segments.Last(); - } - string[] IModuleDependenciesProvider.GetCssDependencies(string filename) { var entriesFilePath = Path.GetFileNameWithoutExtension(filename); if (!dependencies.ContainsKey(entriesFilePath)) { - return new string[0]; + return Array.Empty(); } RefreshDependencies(); @@ -48,7 +41,7 @@ string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { var entriesFilePath = Path.GetFileNameWithoutExtension(filename); if (!dependencies.ContainsKey(entriesFilePath)) { - return new string[0]; + return Array.Empty(); } RefreshDependencies(); @@ -59,23 +52,21 @@ string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { .ToArray(); } - public void RefreshDependencies() { - var shouldRefresh = true; - + private void RefreshDependencies() { var timeSpan = DateTime.Now.Subtract(lastRefresh); if (timeSpan.TotalSeconds < 5) { - shouldRefresh = false; + return; } - if (shouldRefresh) { - using var httpClient = new HttpClient(); - var assembly = typeof(Program).Assembly.GetName().Name; - // var json = httpClient.GetStringAsync(new Uri(uri, $"{assembly}/{ManifestPath}")); - var json = httpClient.GetStringAsync(new Uri(uri, ManifestPath)); - json.Wait(); + using var httpClient = new HttpClient(); + var json = httpClient.GetStringAsync(new Uri(uri, ManifestPath)); + json.Wait(); - dependencies = JsonSerializer.Deserialize>>(json.Result); - lastRefresh = DateTime.Now; - } + dependencies = JsonSerializer.Deserialize>>(json.Result); + lastRefresh = DateTime.Now; + } + + private string GetBaseSegmentFromUri() { + return uri.ToString(); } } diff --git a/Sample.Avalonia/MainView/MainView.scss b/Sample.Avalonia/MainView/MainView.scss index c77b3418..5e401336 100644 --- a/Sample.Avalonia/MainView/MainView.scss +++ b/Sample.Avalonia/MainView/MainView.scss @@ -9,7 +9,6 @@ body { background-position: center; background-size: contain; overflow-y: hidden; - background-image: url("./Tasks.png"); } .title { From 37ab8b29c6daead92246f88d9203d9aac9df2ad9 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 13:31:07 +0000 Subject: [PATCH 20/33] Fixed images usage in examples --- Sample.Avalonia/MainView/MainView.tsx | 6 ++ Sample.Avalonia/package-lock.json | 74 +++++++++++++++++++++++ Sample.Avalonia/package.json | 8 +++ Tests.ReactView/TestAppView/TestApp.tsx | 2 +- Tests.ReactView/package-lock.json | 80 +++++++++++++++++++++++++ Tests.ReactView/package.json | 8 +++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 Sample.Avalonia/package-lock.json create mode 100644 Sample.Avalonia/package.json create mode 100644 Tests.ReactView/package-lock.json create mode 100644 Tests.ReactView/package.json diff --git a/Sample.Avalonia/MainView/MainView.tsx b/Sample.Avalonia/MainView/MainView.tsx index 56757c2b..e9a086a3 100644 --- a/Sample.Avalonia/MainView/MainView.tsx +++ b/Sample.Avalonia/MainView/MainView.tsx @@ -4,6 +4,7 @@ import ViewPlugin from "./../ViewPlugin/ViewPlugin"; import { IPluginsContext } from "PluginsProvider"; import "./MainView.scss"; // import a stylesheet import TaskListView from "./../TaskListView/TaskListView"; // import another component +import * as BackgroundImage from "./Tasks.png"; // import images export interface ITaskCreationDetails { text: string; @@ -73,6 +74,11 @@ export default class MainView extends React.Component { diff --git a/Sample.Avalonia/package-lock.json b/Sample.Avalonia/package-lock.json new file mode 100644 index 00000000..824aa408 --- /dev/null +++ b/Sample.Avalonia/package-lock.json @@ -0,0 +1,74 @@ +{ + "name": "sample.avalonia", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sample.avalonia", + "version": "1.0.0", + "devDependencies": { + "@types/react": "16.9.0", + "@types/react-dom": "16.9.0" + } + }, + "node_modules/@types/prop-types": { + "dev": true + }, + "node_modules/@types/react": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", + "integrity": "sha512-eOct1hyZI9YZf/eqNlYu7jxA9qyTw1EGXruAJhHhBDBpc00W0C1vwlnh+hkOf7UFZkNK+UxnFBpwAZe3d7XJhQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "node_modules/@types/react-dom": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz", + "integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + } + }, + "dependencies": { + "@types/prop-types": { + "dev": true + }, + "@types/react": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", + "integrity": "sha512-eOct1hyZI9YZf/eqNlYu7jxA9qyTw1EGXruAJhHhBDBpc00W0C1vwlnh+hkOf7UFZkNK+UxnFBpwAZe3d7XJhQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz", + "integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + } + } +} diff --git a/Sample.Avalonia/package.json b/Sample.Avalonia/package.json new file mode 100644 index 00000000..128b79aa --- /dev/null +++ b/Sample.Avalonia/package.json @@ -0,0 +1,8 @@ +{ + "name": "sample.avalonia", + "version": "1.0.0", + "devDependencies": { + "@types/react": "16.9.0", + "@types/react-dom": "16.9.0" + } +} diff --git a/Tests.ReactView/TestAppView/TestApp.tsx b/Tests.ReactView/TestAppView/TestApp.tsx index a2249658..84983a16 100644 --- a/Tests.ReactView/TestAppView/TestApp.tsx +++ b/Tests.ReactView/TestAppView/TestApp.tsx @@ -68,7 +68,7 @@ class App extends React.Component { function getText(stylesheet: CSSStyleSheet): string { return Array.from(stylesheet.rules).map(rule => { if (rule instanceof CSSImportRule) { - return getText(rule.styleSheet); + return getText(rule.styleSheet as CSSStyleSheet); } else { return rule.cssText; } diff --git a/Tests.ReactView/package-lock.json b/Tests.ReactView/package-lock.json new file mode 100644 index 00000000..2ef98bcf --- /dev/null +++ b/Tests.ReactView/package-lock.json @@ -0,0 +1,80 @@ +{ + "name": "tests.reactview", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tests.reactview", + "version": "1.0.0", + "devDependencies": { + "@types/react": "16.9.0", + "@types/react-dom": "16.9.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "dev": true + }, + "node_modules/@types/react": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", + "integrity": "sha512-eOct1hyZI9YZf/eqNlYu7jxA9qyTw1EGXruAJhHhBDBpc00W0C1vwlnh+hkOf7UFZkNK+UxnFBpwAZe3d7XJhQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "node_modules/@types/react-dom": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz", + "integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + } + }, + "dependencies": { + "@types/prop-types": { + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "dev": true + }, + "@types/react": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", + "integrity": "sha512-eOct1hyZI9YZf/eqNlYu7jxA9qyTw1EGXruAJhHhBDBpc00W0C1vwlnh+hkOf7UFZkNK+UxnFBpwAZe3d7XJhQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz", + "integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "dev": true + } + } +} diff --git a/Tests.ReactView/package.json b/Tests.ReactView/package.json new file mode 100644 index 00000000..a0b6cd3f --- /dev/null +++ b/Tests.ReactView/package.json @@ -0,0 +1,8 @@ +{ + "name": "tests.reactview", + "version": "1.0.0", + "devDependencies": { + "@types/react": "16.9.0", + "@types/react-dom": "16.9.0" + } +} From f2cd1c88c013f23c18a5662674c5f208ba3ccbbe Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 13:35:52 +0000 Subject: [PATCH 21/33] Fixed ViewGenerator --- ViewGenerator/ViewGenerator.csproj | 2 +- ViewGenerator/tools/webpack/Plugins/Utils.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ViewGenerator/ViewGenerator.csproj b/ViewGenerator/ViewGenerator.csproj index 2ff96d33..588f8b92 100644 --- a/ViewGenerator/ViewGenerator.csproj +++ b/ViewGenerator/ViewGenerator.csproj @@ -7,7 +7,7 @@ ViewGenerator ViewGenerator Generates .NET View bindings from typescript - 1.1.19 + 1.1.20 ViewGenerator Library diff --git a/ViewGenerator/tools/webpack/Plugins/Utils.ts b/ViewGenerator/tools/webpack/Plugins/Utils.ts index 18e216c3..6fc401c2 100644 --- a/ViewGenerator/tools/webpack/Plugins/Utils.ts +++ b/ViewGenerator/tools/webpack/Plugins/Utils.ts @@ -110,5 +110,8 @@ export function getFileName(relativePaths: Dictionary, chunkData: any): * Sanitizes a command-line parameter * */ export function sanitizeCommandLineParam(parameter: string): string { + if (!parameter) { + return ""; + } return parameter.replaceAll("'", ""); } \ No newline at end of file From 5b86d0a3b6663e373ef06907b2b29ed33a5af584 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 13:38:57 +0000 Subject: [PATCH 22/33] Fixed WPF build --- ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj | 2 ++ .../FileDependenciesProvider.cs | 0 .../IModuleDependenciesProvider.cs | 0 3 files changed, 2 insertions(+) rename {ReactViewControl.Avalonia => ReactViewControl}/FileDependenciesProvider.cs (100%) rename {ReactViewControl.Avalonia => ReactViewControl}/IModuleDependenciesProvider.cs (100%) diff --git a/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj b/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj index 2a7a39b8..742be152 100644 --- a/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj +++ b/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj @@ -34,6 +34,8 @@ + + diff --git a/ReactViewControl.Avalonia/FileDependenciesProvider.cs b/ReactViewControl/FileDependenciesProvider.cs similarity index 100% rename from ReactViewControl.Avalonia/FileDependenciesProvider.cs rename to ReactViewControl/FileDependenciesProvider.cs diff --git a/ReactViewControl.Avalonia/IModuleDependenciesProvider.cs b/ReactViewControl/IModuleDependenciesProvider.cs similarity index 100% rename from ReactViewControl.Avalonia/IModuleDependenciesProvider.cs rename to ReactViewControl/IModuleDependenciesProvider.cs From cb50b33360fb4c7ce0447bd4e4a9d03b125deb6d Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 13:56:59 +0000 Subject: [PATCH 23/33] Fixed build --- .gitignore | 1 + Sample.Avalonia/Sample.Avalonia.csproj | 35 +++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fad989fa..17a4979a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ Sample.Avalonia/Generated/ Sample.Avalonia/node_modules/ **/*.DS_Store **/*.idea +**/RunWebpackDevServer.sh \ No newline at end of file diff --git a/Sample.Avalonia/Sample.Avalonia.csproj b/Sample.Avalonia/Sample.Avalonia.csproj index 6d34bc3b..08263b9b 100644 --- a/Sample.Avalonia/Sample.Avalonia.csproj +++ b/Sample.Avalonia/Sample.Avalonia.csproj @@ -8,6 +8,11 @@ osx-x64;win-x64;osx-arm64;win-arm64 + + false + false + + @@ -59,5 +64,33 @@ True - + + + + +@echo off +set VIEW_GENERATOR_PATH=$(PkgViewGenerator) +set VIEW_PACKER_PATH=$(PkgViewPacker) + +node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-server.js" --config="%VIEW_GENERATOR_PATH%\tools\webpack\webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static "$(ProjectDir)" + + + + + + + + RunWebpackDevServer.sh + $(ProjectDir)$(WebpackDevServerScriptName) + +#! /bin/bash +VIEW_GENERATOR_PATH=$(PkgViewGenerator) +VIEW_PACKER_PATH=$(PkgViewPacker) + +node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static "$(ProjectDir)" + + + + + From 024a7bba9d4eec28135e78dad31d9a52a9b5eda7 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 14:03:12 +0000 Subject: [PATCH 24/33] Fixed build --- Tests.ReactView/Tests.ReactView.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests.ReactView/Tests.ReactView.csproj b/Tests.ReactView/Tests.ReactView.csproj index f534c121..1bca6b5e 100644 --- a/Tests.ReactView/Tests.ReactView.csproj +++ b/Tests.ReactView/Tests.ReactView.csproj @@ -11,6 +11,11 @@ win-x64;win-arm64 + + false + false + + From b939010bab16d1bb1bd3629ce80f5e9f3ea32d43 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 14:20:43 +0000 Subject: [PATCH 25/33] Fixed script --- Sample.Avalonia/Sample.Avalonia.csproj | 4 ++-- Sample.Avalonia/package-lock.json | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Sample.Avalonia/Sample.Avalonia.csproj b/Sample.Avalonia/Sample.Avalonia.csproj index 08263b9b..f0bd630f 100644 --- a/Sample.Avalonia/Sample.Avalonia.csproj +++ b/Sample.Avalonia/Sample.Avalonia.csproj @@ -72,7 +72,7 @@ set VIEW_GENERATOR_PATH=$(PkgViewGenerator) set VIEW_PACKER_PATH=$(PkgViewPacker) -node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-server.js" --config="%VIEW_GENERATOR_PATH%\tools\webpack\webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static "$(ProjectDir)" +node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-server.js" --config="%VIEW_GENERATOR_PATH%\tools\webpack\webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" @@ -87,7 +87,7 @@ node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-s VIEW_GENERATOR_PATH=$(PkgViewGenerator) VIEW_PACKER_PATH=$(PkgViewPacker) -node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static "$(ProjectDir)" +node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" diff --git a/Sample.Avalonia/package-lock.json b/Sample.Avalonia/package-lock.json index 824aa408..6a7e15f7 100644 --- a/Sample.Avalonia/package-lock.json +++ b/Sample.Avalonia/package-lock.json @@ -12,9 +12,6 @@ "@types/react-dom": "16.9.0" } }, - "node_modules/@types/prop-types": { - "dev": true - }, "node_modules/@types/react": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", @@ -42,9 +39,6 @@ } }, "dependencies": { - "@types/prop-types": { - "dev": true - }, "@types/react": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.0.tgz", From e5159f5085b1f7bd38de7e4db41ec811eca9f583 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 14:56:38 +0000 Subject: [PATCH 26/33] Tentative tests fix --- Tests.ReactView/Tests.ReactView.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests.ReactView/Tests.ReactView.csproj b/Tests.ReactView/Tests.ReactView.csproj index 1bca6b5e..de365f57 100644 --- a/Tests.ReactView/Tests.ReactView.csproj +++ b/Tests.ReactView/Tests.ReactView.csproj @@ -14,6 +14,7 @@ false false + false From 3601f20d9f5b7a0adb50d1f547b4c1340e676615 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 13 Nov 2023 19:06:26 +0000 Subject: [PATCH 27/33] Fixed tests --- .gitignore | 3 +- ReactViewControl/ViewModuleContainer.cs | 4 ++- Tests.ReactView/AliasedModule.cs | 23 +++++++++++++ Tests.ReactView/PluginModule.cs | 23 +++++++++++++ Tests.ReactView/PluginModulesLoadTests.cs | 40 ----------------------- Tests.ReactView/TestReactViewFactory.cs | 4 +++ 6 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 Tests.ReactView/AliasedModule.cs create mode 100644 Tests.ReactView/PluginModule.cs diff --git a/.gitignore b/.gitignore index 17a4979a..4099e5e5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ Sample.Avalonia/Generated/ Sample.Avalonia/node_modules/ **/*.DS_Store **/*.idea -**/RunWebpackDevServer.sh \ No newline at end of file +**/RunWebpackDevServer.sh +**/RunWebpackDevServer.bat \ No newline at end of file diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index 44e84e7c..dc2fa1a3 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -50,7 +50,9 @@ void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost, IModuleDepende frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); this.frame = frame; this.childViewHost = childViewHost; - this.dependenciesProvider = moduleDependenciesProvider; + if (moduleDependenciesProvider != null) { + this.dependenciesProvider = moduleDependenciesProvider; + } } // ease access in generated code diff --git a/Tests.ReactView/AliasedModule.cs b/Tests.ReactView/AliasedModule.cs new file mode 100644 index 00000000..a1982e59 --- /dev/null +++ b/Tests.ReactView/AliasedModule.cs @@ -0,0 +1,23 @@ +using ReactViewControl; + +namespace Tests.ReactView; + +public class AliasedModule : ViewModuleContainer { + + internal interface IProperties { + } + + private class Properties : IProperties { + protected AliasedModule Owner { get; } + public Properties(AliasedModule owner) { + Owner = owner; + } + } + + protected override string MainJsSource => "/Tests.ReactView/Generated/AliasedModule.js"; + protected override string NativeObjectName => nameof(AliasedModule); + protected override string ModuleName => "AliasedModule"; + protected override object CreateNativeObject() { + return new Properties(this); + } +} diff --git a/Tests.ReactView/PluginModule.cs b/Tests.ReactView/PluginModule.cs new file mode 100644 index 00000000..26b9fbad --- /dev/null +++ b/Tests.ReactView/PluginModule.cs @@ -0,0 +1,23 @@ +using ReactViewControl; + +namespace Tests.ReactView; + +public class PluginModule : ViewModuleContainer { + + internal interface IProperties { + } + + private class Properties : IProperties { + protected PluginModule Owner { get; } + public Properties(PluginModule owner) { + Owner = owner; + } + } + + protected override string MainJsSource => "/Tests.ReactView/Generated/PluginModule.js"; + protected override string NativeObjectName => nameof(PluginModule); + protected override string ModuleName => "PluginModule"; + protected override object CreateNativeObject() { + return new Properties(this); + } +} diff --git a/Tests.ReactView/PluginModulesLoadTests.cs b/Tests.ReactView/PluginModulesLoadTests.cs index d5ba8ee2..e300eabc 100644 --- a/Tests.ReactView/PluginModulesLoadTests.cs +++ b/Tests.ReactView/PluginModulesLoadTests.cs @@ -24,46 +24,6 @@ protected override TestReactView CreateView() { return new ReactViewWithPlugin(); } - class PluginModule : ViewModuleContainer { - - internal interface IProperties { - } - - private class Properties : IProperties { - protected PluginModule Owner { get; } - public Properties(PluginModule owner) { - Owner = owner; - } - } - - protected override string MainJsSource => "/Tests.ReactView/Generated/PluginModule.js"; - protected override string NativeObjectName => nameof(PluginModule); - protected override string ModuleName => "PluginModule"; - protected override object CreateNativeObject() { - return new Properties(this); - } - } - - class AliasedModule : ViewModuleContainer { - - internal interface IProperties { - } - - private class Properties : IProperties { - protected AliasedModule Owner { get; } - public Properties(AliasedModule owner) { - Owner = owner; - } - } - - protected override string MainJsSource => "/Tests.ReactView/Generated/AliasedModule.js"; - protected override string NativeObjectName => nameof(AliasedModule); - protected override string ModuleName => "AliasedModule"; - protected override object CreateNativeObject() { - return new Properties(this); - } - } - [Test(Description = "Tests plugin module is loaded")] public async Task PluginModuleIsLoaded() { await Run(async () => { diff --git a/Tests.ReactView/TestReactViewFactory.cs b/Tests.ReactView/TestReactViewFactory.cs index 3fa953aa..462b8199 100644 --- a/Tests.ReactView/TestReactViewFactory.cs +++ b/Tests.ReactView/TestReactViewFactory.cs @@ -5,5 +5,9 @@ namespace Tests.ReactView { public class TestReactViewFactory : ReactViewFactory { public override bool EnableViewPreload => false; + + public override IViewModule[] InitializePlugins() { + return new IViewModule[] { new PluginModule(), new AliasedModule() }; + } } } From 09b177331fed09d0eafab296a5e87ad08e00a2a6 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Tue, 5 Dec 2023 13:36:00 +0000 Subject: [PATCH 28/33] Tentative tests fix --- Tests.ReactView/TestAppView/TestApp.tsx | 6 +++--- Tests.ReactView/TestReactViewFactory.cs | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests.ReactView/TestAppView/TestApp.tsx b/Tests.ReactView/TestAppView/TestApp.tsx index 84983a16..52a8afb1 100644 --- a/Tests.ReactView/TestAppView/TestApp.tsx +++ b/Tests.ReactView/TestAppView/TestApp.tsx @@ -1,5 +1,4 @@ -import dummy from "ModuleWithAlias"; -import { IPluginsContext } from 'PluginsProvider'; +import { IPluginsContext } from 'PluginsProvider'; import * as React from 'react'; import { ViewFrame, ViewSharedContext } from "ViewFrame"; import * as Image from "./imgs/image.png"; @@ -7,6 +6,7 @@ import InnerView from "./InnerView"; import Plugin from './PluginModule'; import "./Styles.scss"; import { Task } from "./Task"; +import * as ModuleWithAlias from "ModuleWithAlias"; interface IAppProperties { event: (args: string) => void; @@ -92,7 +92,7 @@ class App extends React.Component { } checkAliasedModuleLoaded() { - if (dummy()) { + if (ModuleWithAlias.default()) { this.props.event("AliasedModuleLoaded"); } } diff --git a/Tests.ReactView/TestReactViewFactory.cs b/Tests.ReactView/TestReactViewFactory.cs index 462b8199..3fa953aa 100644 --- a/Tests.ReactView/TestReactViewFactory.cs +++ b/Tests.ReactView/TestReactViewFactory.cs @@ -5,9 +5,5 @@ namespace Tests.ReactView { public class TestReactViewFactory : ReactViewFactory { public override bool EnableViewPreload => false; - - public override IViewModule[] InitializePlugins() { - return new IViewModule[] { new PluginModule(), new AliasedModule() }; - } } } From 36bf792d70a59eb5ee66fe783ba92a7755a4a35a Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Tue, 5 Dec 2023 13:39:10 +0000 Subject: [PATCH 29/33] Tentative fix for tests --- Tests.ReactView/TestAppView/TestApp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests.ReactView/TestAppView/TestApp.tsx b/Tests.ReactView/TestAppView/TestApp.tsx index 52a8afb1..632360aa 100644 --- a/Tests.ReactView/TestAppView/TestApp.tsx +++ b/Tests.ReactView/TestAppView/TestApp.tsx @@ -6,7 +6,6 @@ import InnerView from "./InnerView"; import Plugin from './PluginModule'; import "./Styles.scss"; import { Task } from "./Task"; -import * as ModuleWithAlias from "ModuleWithAlias"; interface IAppProperties { event: (args: string) => void; @@ -92,7 +91,8 @@ class App extends React.Component { } checkAliasedModuleLoaded() { - if (ModuleWithAlias.default()) { + // Access plugin via window object so that we are not forced to load plugins for all tests - otherwise it will be required from webpack to load AliasedModule + if ((window as any).Views.AliasedModule.default()) { this.props.event("AliasedModuleLoaded"); } } From 316fcedb500620a02168488e80fdbd00de05293e Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 11 Dec 2023 16:49:45 +0000 Subject: [PATCH 30/33] Code review refactor --- .../ReactViewControl.Avalonia.csproj | 2 ++ ReactViewControl/FileDependenciesProvider.cs | 11 +++---- .../IModuleDependenciesProvider.cs | 4 +-- .../IModuleDependenciesProviderFactory.cs | 6 ++++ ReactViewControl/IViewModule.cs | 4 +-- .../ModuleDependenciesProviderFactory.cs | 14 +++++++++ ReactViewControl/ReactView.cs | 2 +- ReactViewControl/ReactViewFactory.cs | 5 ---- ReactViewControl/ReactViewRender.cs | 11 +++---- ReactViewControl/ViewModuleContainer.cs | 17 ++++------- .../GenerateWebpackDevServerScript.targets | 30 +++++++++++++++++++ Sample.Avalonia/ExtendedReactView.cs | 7 +++-- Sample.Avalonia/ExtendedReactViewFactory.cs | 16 +--------- .../HotReloadDependenciesProvider.cs | 20 ++++++------- .../HotReloadDependenciesProviderFactory.cs | 17 +++++++++++ Sample.Avalonia/Sample.Avalonia.csproj | 29 +----------------- ViewGenerator/ViewGenerator.csproj | 2 +- ViewGenerator/build/ViewGenerator.targets | 4 +-- 18 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 ReactViewControl/IModuleDependenciesProviderFactory.cs create mode 100644 ReactViewControl/ModuleDependenciesProviderFactory.cs create mode 100644 Sample.Avalonia/Build/GenerateWebpackDevServerScript.targets create mode 100644 Sample.Avalonia/HotReloadDependenciesProviderFactory.cs diff --git a/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj b/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj index 742be152..62f0f7ff 100644 --- a/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj +++ b/ReactViewControl.Avalonia/ReactViewControl.Avalonia.csproj @@ -34,6 +34,8 @@ + + diff --git a/ReactViewControl/FileDependenciesProvider.cs b/ReactViewControl/FileDependenciesProvider.cs index 98998481..99ba1fac 100644 --- a/ReactViewControl/FileDependenciesProvider.cs +++ b/ReactViewControl/FileDependenciesProvider.cs @@ -12,14 +12,15 @@ internal class FileDependenciesProvider : IModuleDependenciesProvider { private readonly string sourcePath; public FileDependenciesProvider(string sourcePath) { + this.sourcePath = sourcePath; + DependencyJsSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(JsEntryFileExtension)); CssSourcesCache = new Lazy(() => GetDependenciesFromEntriesFile(CssEntryFileExtension)); - this.sourcePath = sourcePath; } - public string[] GetCssDependencies(string filename) => CssSourcesCache.Value; + public string[] GetCssDependencies() => CssSourcesCache.Value; - public string[] GetJsDependencies(string filename) => DependencyJsSourcesCache.Value; + public string[] GetJsDependencies() => DependencyJsSourcesCache.Value; private Lazy DependencyJsSourcesCache { get; } private Lazy CssSourcesCache { get; } @@ -32,13 +33,13 @@ private string[] GetDependenciesFromEntriesFile(string extension) { if (stream != null) { using (var reader = new StreamReader(stream)) { var allEntries = reader.ReadToEnd(); - if (allEntries != null && allEntries != string.Empty) { + if (string.IsNullOrEmpty(allEntries)) { return allEntries.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } } } } - return new string[0]; + return Array.Empty(); } private Stream GetResourceStream(string[] resource) => ResourcesManager.TryGetResourceWithFullPath(resource.First(), resource); diff --git a/ReactViewControl/IModuleDependenciesProvider.cs b/ReactViewControl/IModuleDependenciesProvider.cs index bcec00a7..2388ec72 100644 --- a/ReactViewControl/IModuleDependenciesProvider.cs +++ b/ReactViewControl/IModuleDependenciesProvider.cs @@ -1,7 +1,7 @@ namespace ReactViewControl { public interface IModuleDependenciesProvider { - string[] GetCssDependencies(string filename); + string[] GetCssDependencies(); - string[] GetJsDependencies(string filename); + string[] GetJsDependencies(); } } \ No newline at end of file diff --git a/ReactViewControl/IModuleDependenciesProviderFactory.cs b/ReactViewControl/IModuleDependenciesProviderFactory.cs new file mode 100644 index 00000000..0f497a7d --- /dev/null +++ b/ReactViewControl/IModuleDependenciesProviderFactory.cs @@ -0,0 +1,6 @@ +namespace ReactViewControl; + +public interface IModuleDependenciesProviderFactory { + + IModuleDependenciesProvider CreateModuleDependenciesProvider(string sourcePath); +} diff --git a/ReactViewControl/IViewModule.cs b/ReactViewControl/IViewModule.cs index 0960281c..cec421d0 100644 --- a/ReactViewControl/IViewModule.cs +++ b/ReactViewControl/IViewModule.cs @@ -6,8 +6,6 @@ public interface IViewModule { string MainJsSource { get; } - IModuleDependenciesProvider DependenciesProvider { get; } - string[] DependencyJsSources { get; } string[] CssSources { get; } @@ -24,7 +22,7 @@ public interface IViewModule { KeyValuePair[] PropertiesValues { get; } - void Bind(IFrame frame, IChildViewHost host = null, IModuleDependenciesProvider moduleDependenciesProvider = null); + void Bind(IFrame frame, IChildViewHost host = null); event CustomResourceRequestedEventHandler CustomResourceRequested; diff --git a/ReactViewControl/ModuleDependenciesProviderFactory.cs b/ReactViewControl/ModuleDependenciesProviderFactory.cs new file mode 100644 index 00000000..6519a8c7 --- /dev/null +++ b/ReactViewControl/ModuleDependenciesProviderFactory.cs @@ -0,0 +1,14 @@ +namespace ReactViewControl; + +public class ModuleDependenciesProviderFactory : IModuleDependenciesProviderFactory { + + public static IModuleDependenciesProviderFactory Instance { get; private set; } = new ModuleDependenciesProviderFactory(); + + public static void SetInstance(IModuleDependenciesProviderFactory factory) { + Instance = factory; + } + + public virtual IModuleDependenciesProvider CreateModuleDependenciesProvider(string sourcePath) { + return new FileDependenciesProvider(sourcePath); + } +} diff --git a/ReactViewControl/ReactView.cs b/ReactViewControl/ReactView.cs index b5b0dfaf..fa769c16 100644 --- a/ReactViewControl/ReactView.cs +++ b/ReactViewControl/ReactView.cs @@ -19,7 +19,7 @@ public abstract partial class ReactView : IDisposable { private static ReactViewRender CreateReactViewInstance(ReactViewFactory factory) { ReactViewRender InnerCreateView() { - var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.ModuleDependenciesProvider); + var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode); if (factory.ShowDeveloperTools) { view.ShowDeveloperTools(); } diff --git a/ReactViewControl/ReactViewFactory.cs b/ReactViewControl/ReactViewFactory.cs index a3bc43c2..156f2057 100644 --- a/ReactViewControl/ReactViewFactory.cs +++ b/ReactViewControl/ReactViewFactory.cs @@ -30,10 +30,5 @@ public class ReactViewFactory { /// The view is cached and preloaded. First render occurs earlier. /// public virtual bool EnableViewPreload => true; - - /// - /// Module dependencies provider. - /// - public virtual IModuleDependenciesProvider ModuleDependenciesProvider => null; } } \ No newline at end of file diff --git a/ReactViewControl/ReactViewRender.cs b/ReactViewControl/ReactViewRender.cs index 330d399d..f87bb4a2 100644 --- a/ReactViewControl/ReactViewRender.cs +++ b/ReactViewControl/ReactViewRender.cs @@ -36,7 +36,7 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable { private ResourceUrl defaultStyleSheet; private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready) - public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode, IModuleDependenciesProvider moduleDependenciesProvider = null) { + public ReactViewRender(ResourceUrl defaultStyleSheet, Func initializePlugins, bool preloadWebView, bool enableDebugMode) { UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly; // must useSharedDomain for the local storage to be shared @@ -53,7 +53,6 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func initia DefaultStyleSheet = defaultStyleSheet; PluginsFactory = initializePlugins; EnableDebugMode = enableDebugMode; - ModuleDependenciesProvider = moduleDependenciesProvider; GetOrCreateFrame(FrameInfo.MainViewFrameName); // creates the main frame @@ -120,8 +119,6 @@ public bool EnableDebugMode { } } - public IModuleDependenciesProvider ModuleDependenciesProvider { get; } - /// /// Gets or sets the webview zoom percentage (1 = 100%) /// @@ -275,7 +272,7 @@ private void TryLoadComponent(FrameInfo frame) { if (isInputDisabled && frame.IsMain) { Loader.DisableMouseInteractions(); } - } + } /// /// Gets or sets the url of the default stylesheet. @@ -304,7 +301,7 @@ private void AddPlugins(IViewModule[] plugins, FrameInfo frame) { frame.Plugins = frame.Plugins.Concat(plugins).ToArray(); foreach (var plugin in plugins) { - plugin.Bind(frame, moduleDependenciesProvider: ModuleDependenciesProvider); + plugin.Bind(frame); } } @@ -405,7 +402,7 @@ public bool AddChildView(IViewModule childView, string frameName) { /// private void BindComponentToFrame(IViewModule component, FrameInfo frame) { frame.Component = component; - component.Bind(frame, this, ModuleDependenciesProvider); + component.Bind(frame, this); } /// diff --git a/ReactViewControl/ViewModuleContainer.cs b/ReactViewControl/ViewModuleContainer.cs index dc2fa1a3..e7344aee 100644 --- a/ReactViewControl/ViewModuleContainer.cs +++ b/ReactViewControl/ViewModuleContainer.cs @@ -6,16 +6,14 @@ namespace ReactViewControl { public abstract class ViewModuleContainer : IViewModule { private IFrame frame; private IChildViewHost childViewHost; - private IModuleDependenciesProvider dependenciesProvider; + + private Lazy DependenciesProvider => new(() => + ModuleDependenciesProviderFactory.Instance.CreateModuleDependenciesProvider(MainJsSource)); protected ViewModuleContainer() { frame = new FrameInfo("dummy"); } - public virtual IModuleDependenciesProvider DependenciesProvider { - get { return dependenciesProvider ??= new FileDependenciesProvider(MainJsSource); } - } - protected virtual string MainJsSource => null; protected virtual string NativeObjectName => null; protected virtual string ModuleName => null; @@ -39,20 +37,17 @@ public virtual IModuleDependenciesProvider DependenciesProvider { string[] IViewModule.Events => Events; - string[] IViewModule.DependencyJsSources => DependenciesProvider.GetJsDependencies(MainJsSource); + string[] IViewModule.DependencyJsSources => DependenciesProvider.Value.GetJsDependencies(); - string[] IViewModule.CssSources => DependenciesProvider.GetCssDependencies(MainJsSource); + string[] IViewModule.CssSources => DependenciesProvider.Value.GetCssDependencies(); KeyValuePair[] IViewModule.PropertiesValues => PropertiesValues; - void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost, IModuleDependenciesProvider moduleDependenciesProvider) { + void IViewModule.Bind(IFrame frame, IChildViewHost childViewHost) { frame.CustomResourceRequestedHandler += this.frame.CustomResourceRequestedHandler; frame.ExecutionEngine.MergeWorkload(this.frame.ExecutionEngine); this.frame = frame; this.childViewHost = childViewHost; - if (moduleDependenciesProvider != null) { - this.dependenciesProvider = moduleDependenciesProvider; - } } // ease access in generated code diff --git a/Sample.Avalonia/Build/GenerateWebpackDevServerScript.targets b/Sample.Avalonia/Build/GenerateWebpackDevServerScript.targets new file mode 100644 index 00000000..01d43061 --- /dev/null +++ b/Sample.Avalonia/Build/GenerateWebpackDevServerScript.targets @@ -0,0 +1,30 @@ + + + + + @echo off + set VIEW_GENERATOR_PATH=$(PkgViewGenerator) + set VIEW_PACKER_PATH=$(PkgViewPacker) + + node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-server.js" --config="%VIEW_GENERATOR_PATH%\tools\webpack\webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" + + + + + + + + RunWebpackDevServer.sh + $(ProjectDir)$(WebpackDevServerScriptName) + + #! /bin/bash + VIEW_GENERATOR_PATH=$(PkgViewGenerator) + VIEW_PACKER_PATH=$(PkgViewPacker) + + node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" + + + + + + \ No newline at end of file diff --git a/Sample.Avalonia/ExtendedReactView.cs b/Sample.Avalonia/ExtendedReactView.cs index 4e4f1159..b1c3f873 100644 --- a/Sample.Avalonia/ExtendedReactView.cs +++ b/Sample.Avalonia/ExtendedReactView.cs @@ -10,9 +10,12 @@ public partial class ExtendedReactView : ReactView { static ExtendedReactView() { var uri = Environment.GetEnvironmentVariable("DEV_SERVER_URI"); - if (uri != null) { - DevServerUri = new Uri(uri); + if (uri == null) { + return; } + + DevServerUri = new Uri(uri); + ModuleDependenciesProviderFactory.SetInstance(new HotReloadDependenciesProviderFactory(uri)); } private static void OnEmbeddedResourceRequested(WebViewControl.ResourceHandler resourceHandler) { diff --git a/Sample.Avalonia/ExtendedReactViewFactory.cs b/Sample.Avalonia/ExtendedReactViewFactory.cs index baa90cac..79324c0e 100644 --- a/Sample.Avalonia/ExtendedReactViewFactory.cs +++ b/Sample.Avalonia/ExtendedReactViewFactory.cs @@ -1,5 +1,4 @@ -using System; -using ReactViewControl; +using ReactViewControl; using WebViewControl; namespace Sample.Avalonia { @@ -23,19 +22,6 @@ public override IViewModule[] InitializePlugins() { #if DEBUG public override bool EnableDebugMode => true; - - private static HotReloadDependenciesProvider hotReloadDependenciesProvider; - - public override IModuleDependenciesProvider ModuleDependenciesProvider { - get { - if (DevServerUri != null) { - return hotReloadDependenciesProvider ??= new HotReloadDependenciesProvider( - new Uri($"{DevServerUri}{typeof(ExtendedReactViewFactory).Assembly.GetName().Name}/")); - } - - return base.ModuleDependenciesProvider; - } - } #endif } } diff --git a/Sample.Avalonia/HotReloadDependenciesProvider.cs b/Sample.Avalonia/HotReloadDependenciesProvider.cs index a18e2e20..1a9f0880 100644 --- a/Sample.Avalonia/HotReloadDependenciesProvider.cs +++ b/Sample.Avalonia/HotReloadDependenciesProvider.cs @@ -13,39 +13,37 @@ public class HotReloadDependenciesProvider : IModuleDependenciesProvider { private Dictionary> dependencies; private readonly Uri uri; private readonly string basePath; + private readonly string entryName; private const string ManifestPath = "manifest.json"; private DateTime lastRefresh; - public HotReloadDependenciesProvider(Uri uri) { + public HotReloadDependenciesProvider(Uri uri, string sourcePath) { this.uri = uri; basePath = GetBaseSegmentFromUri(); + entryName = Path.GetFileNameWithoutExtension(sourcePath);; RefreshDependencies(); } - string[] IModuleDependenciesProvider.GetCssDependencies(string filename) { - var entriesFilePath = Path.GetFileNameWithoutExtension(filename); - - if (!dependencies.ContainsKey(entriesFilePath)) { + string[] IModuleDependenciesProvider.GetCssDependencies() { + if (!dependencies.ContainsKey(entryName)) { return Array.Empty(); } RefreshDependencies(); - return dependencies[entriesFilePath] + return dependencies[entryName] .Where(dependency => dependency.Contains(".css")) .Select(dependency => basePath + dependency) .ToArray(); } - string[] IModuleDependenciesProvider.GetJsDependencies(string filename) { - var entriesFilePath = Path.GetFileNameWithoutExtension(filename); - - if (!dependencies.ContainsKey(entriesFilePath)) { + string[] IModuleDependenciesProvider.GetJsDependencies() { + if (!dependencies.ContainsKey(entryName)) { return Array.Empty(); } RefreshDependencies(); - return dependencies[entriesFilePath] + return dependencies[entryName] .Where(dependency => dependency.Contains(".js") && !dependency.Contains("ViewsRuntime")) .Select(dependency => basePath + dependency) .Reverse().Skip(1).Reverse().ToArray() // remove self reference diff --git a/Sample.Avalonia/HotReloadDependenciesProviderFactory.cs b/Sample.Avalonia/HotReloadDependenciesProviderFactory.cs new file mode 100644 index 00000000..084f959c --- /dev/null +++ b/Sample.Avalonia/HotReloadDependenciesProviderFactory.cs @@ -0,0 +1,17 @@ +using System; +using ReactViewControl; + +namespace Sample.Avalonia; + +public class HotReloadDependenciesProviderFactory : ModuleDependenciesProviderFactory { + + private readonly string devServerUri; + + public HotReloadDependenciesProviderFactory(string devServerUri) { + this.devServerUri = devServerUri; + } + + public override IModuleDependenciesProvider CreateModuleDependenciesProvider(string sourcePath) { + return new HotReloadDependenciesProvider(new Uri($"{devServerUri}{typeof(HotReloadDependenciesProviderFactory).Assembly.GetName().Name}/"), sourcePath); + } +} \ No newline at end of file diff --git a/Sample.Avalonia/Sample.Avalonia.csproj b/Sample.Avalonia/Sample.Avalonia.csproj index f0bd630f..f91b953c 100644 --- a/Sample.Avalonia/Sample.Avalonia.csproj +++ b/Sample.Avalonia/Sample.Avalonia.csproj @@ -65,32 +65,5 @@ - - - -@echo off -set VIEW_GENERATOR_PATH=$(PkgViewGenerator) -set VIEW_PACKER_PATH=$(PkgViewPacker) - -node "%VIEW_PACKER_PATH%\tools\node_modules\webpack-dev-server\bin\webpack-dev-server.js" --config="%VIEW_GENERATOR_PATH%\tools\webpack\webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" - - - - - - - - RunWebpackDevServer.sh - $(ProjectDir)$(WebpackDevServerScriptName) - -#! /bin/bash -VIEW_GENERATOR_PATH=$(PkgViewGenerator) -VIEW_PACKER_PATH=$(PkgViewPacker) - -node "$VIEW_PACKER_PATH/tools/node_modules/webpack-dev-server/bin/webpack-dev-server.js" --config="$VIEW_GENERATOR_PATH/tools/webpack/webpack_views.config.js" --mode=development --devtool=inline-source-map --env forHotReload=true --env useCache=false --env assemblyName="Sample.Avalonia" --env optimizeBundle=true --static-directory "$(ProjectDir)" --static-public-path "/Sample.Avalonia/" - - - - - + diff --git a/ViewGenerator/ViewGenerator.csproj b/ViewGenerator/ViewGenerator.csproj index 588f8b92..b23c49eb 100644 --- a/ViewGenerator/ViewGenerator.csproj +++ b/ViewGenerator/ViewGenerator.csproj @@ -7,7 +7,7 @@ ViewGenerator ViewGenerator Generates .NET View bindings from typescript - 1.1.20 + 1.1.22 ViewGenerator Library diff --git a/ViewGenerator/build/ViewGenerator.targets b/ViewGenerator/build/ViewGenerator.targets index c0caaf43..6d3c09e9 100644 --- a/ViewGenerator/build/ViewGenerator.targets +++ b/ViewGenerator/build/ViewGenerator.targets @@ -73,12 +73,12 @@ webpack_views - + inline-source-map true - + false false From c702e9e640bd2dcd90196768f76750927151ccf8 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 11 Dec 2023 17:01:06 +0000 Subject: [PATCH 31/33] CR --- Directory.Packages.props | 2 +- Sample.Avalonia/Sample.Avalonia.csproj | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 68c64770..62796673 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + diff --git a/Sample.Avalonia/Sample.Avalonia.csproj b/Sample.Avalonia/Sample.Avalonia.csproj index f91b953c..6f3e9c7c 100644 --- a/Sample.Avalonia/Sample.Avalonia.csproj +++ b/Sample.Avalonia/Sample.Avalonia.csproj @@ -8,11 +8,6 @@ osx-x64;win-x64;osx-arm64;win-arm64 - - false - false - - From 5fc575ff08ac0eee6336e83c25b912468b3ba5e2 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 11 Dec 2023 17:06:49 +0000 Subject: [PATCH 32/33] Update FileDependenciesProvider.cs --- ReactViewControl/FileDependenciesProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactViewControl/FileDependenciesProvider.cs b/ReactViewControl/FileDependenciesProvider.cs index 99ba1fac..79e82236 100644 --- a/ReactViewControl/FileDependenciesProvider.cs +++ b/ReactViewControl/FileDependenciesProvider.cs @@ -33,7 +33,7 @@ private string[] GetDependenciesFromEntriesFile(string extension) { if (stream != null) { using (var reader = new StreamReader(stream)) { var allEntries = reader.ReadToEnd(); - if (string.IsNullOrEmpty(allEntries)) { + if (!string.IsNullOrEmpty(allEntries)) { return allEntries.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } } From af57090415170c4089dc1088cc88cfc08bb6db98 Mon Sep 17 00:00:00 2001 From: Miguel Alves Date: Mon, 11 Dec 2023 17:19:55 +0000 Subject: [PATCH 33/33] Update Tests.ReactView.csproj --- Tests.ReactView/Tests.ReactView.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests.ReactView/Tests.ReactView.csproj b/Tests.ReactView/Tests.ReactView.csproj index de365f57..3e0ffebd 100644 --- a/Tests.ReactView/Tests.ReactView.csproj +++ b/Tests.ReactView/Tests.ReactView.csproj @@ -11,9 +11,7 @@ win-x64;win-arm64 - - false - false + false