Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ReactViewControl/ReactView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
var view = new ReactViewRender(factory.DefaultStyleSheet, () => factory.InitializePlugins(), factory.EnableViewPreload, factory.EnableDebugMode, factory.EnsureInnerViewsAreDisposed);
if (factory.ShowDeveloperTools) {
view.ShowDeveloperTools();
}
Expand Down
4 changes: 3 additions & 1 deletion ReactViewControl/ReactViewFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ public class ReactViewFactory {
/// The view is cached and preloaded. First render occurs earlier.
/// </summary>
public virtual bool EnableViewPreload => true;

public virtual bool EnsureInnerViewsAreDisposed => true;
}
}
}
3 changes: 2 additions & 1 deletion ReactViewControl/ReactViewRender.LoaderModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public LoaderModule(ReactViewRender viewRender) {
/// <summary>
/// Loads the specified react component into the specified frame
/// </summary>
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins) {
public void LoadComponent(IViewModule component, string frameName, bool hasStyleSheet, bool hasPlugins, bool ensureDisposeInnerViews) {
var mainSource = ViewRender.ToFullUrl(NormalizeUrl(component.MainJsSource));
var dependencySources = component.DependencyJsSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
var cssSources = component.CssSources.Select(s => ViewRender.ToFullUrl(NormalizeUrl(s))).ToArray();
Expand Down Expand Up @@ -56,6 +56,7 @@ public void LoadComponent(IViewModule component, string frameName, bool hasStyle
componentSerialization,
JavascriptSerializer.Serialize(frameName),
JavascriptSerializer.Serialize(componentHash),
JavascriptSerializer.Serialize(ensureDisposeInnerViews),
};

ExecuteLoaderFunction("loadComponent", loadArgs);
Expand Down
10 changes: 6 additions & 4 deletions ReactViewControl/ReactViewRender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ internal partial class ReactViewRender : IChildViewHost, IDisposable {
private bool enableDebugMode;
private ResourceUrl defaultStyleSheet;
private bool isInputDisabled; // used primarly to control the intention to disable input (before the browser is ready)
private readonly bool ensureDisposeInnerViews;

public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initializePlugins, bool preloadWebView, bool enableDebugMode) {
public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initializePlugins, bool preloadWebView, bool enableDebugMode, bool ensureInnerViewsAreDisposed) {
this.ensureDisposeInnerViews = ensureInnerViewsAreDisposed;
UserCallingAssembly = GetUserCallingMethod().ReflectedType.Assembly;

// must useSharedDomain for the local storage to be shared
Expand Down Expand Up @@ -68,12 +70,12 @@ public ReactViewRender(ResourceUrl defaultStyleSheet, Func<IViewModule[]> initia

ExtraInitialize();

var urlParams = new string[] {
var urlParams = new[] {
new ResourceUrl(ResourcesAssembly).ToString(),
enableDebugMode ? "true" : "false",
ExecutionEngine.ModulesObjectName,
NativeAPI.NativeObjectName,
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl
ResourceUrl.CustomScheme + Uri.SchemeDelimiter + CustomResourceBaseUrl,
};

WebView.LoadResource(new ResourceUrl(ResourcesAssembly, ReactViewResources.Resources.DefaultUrl + "?" + string.Join("&", urlParams)));
Expand Down Expand Up @@ -270,7 +272,7 @@ private void TryLoadComponent(FrameInfo frame) {

RegisterNativeObject(frame.Component, frame);

Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0);
Loader.LoadComponent(frame.Component, frame.Name, DefaultStyleSheet != null, frame.Plugins.Length > 0, ensureDisposeInnerViews);
if (isInputDisabled && frame.IsMain) {
Loader.DisableMouseInteractions();
}
Expand Down
21 changes: 15 additions & 6 deletions ReactViewResources/Loader/Internal/Loader.View.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { ViewMetadataContext } from "../Internal/ViewMetadataContext";
import { getEnsureDisposeInnerViewsFlag, ViewMetadataContext } from "../Internal/ViewMetadataContext";
import { PluginsContext, PluginsContextHolder } from "../Public/PluginsContext";
import { formatUrl, ResourceLoader } from "../Public/ResourceLoader";
import { handleError } from "./ErrorHandler";
import { notifyViewDestroyed, notifyViewInitialized } from "./NativeAPI";
import { ViewMetadata } from "./ViewMetadata";
import { ViewPortalsCollection } from "./ViewPortalsCollection";
import { ViewPortalsCollectionLegacy } from "./ViewPortalsCollectionsLegacy";
import { addView, deleteView } from "./ViewsCollection";

export function createView(componentClass: any, properties: {}, view: ViewMetadata, componentName: string) {
componentClass.contextType = PluginsContext;

const makeResourceUrl = (resourceKey: string, ...params: string[]) => formatUrl(view.name, resourceKey, ...params);

return (
<ViewMetadataContext.Provider value={view}>
if(!getEnsureDisposeInnerViewsFlag()) {
return <ViewMetadataContext.Provider value={view}>
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
<ResourceLoader.Provider value={makeResourceUrl}>
<ViewPortalsCollection views={view.childViews}
<ViewPortalsCollectionLegacy views={view.childViews}
viewAdded={onChildViewAdded}
viewRemoved={onChildViewRemoved}
viewErrorRaised={onChildViewErrorRaised} />
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
</ResourceLoader.Provider>
</PluginsContext.Provider>
</ViewMetadataContext.Provider>;
}

return (
<ViewMetadataContext.Provider value={view}>
<PluginsContext.Provider value={new PluginsContextHolder(Array.from(view.modules.values()))}>
<ResourceLoader.Provider value={makeResourceUrl}>
{React.createElement(componentClass, { ref: e => view.modules.set(componentName, e), ...properties })}
</ResourceLoader.Provider>
</PluginsContext.Provider>
</ViewMetadataContext.Provider>
);
}
Expand Down
12 changes: 11 additions & 1 deletion ReactViewResources/Loader/Internal/ViewMetadataContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import * as React from "react";
import { ViewMetadata } from "./ViewMetadata";

export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);
const EnsureDisposeInnerViewsFlagKey = "ENSURE_DISPOSE_INNER_VIEWS";

export const ViewMetadataContext = React.createContext<ViewMetadata>(null!);

export function getEnsureDisposeInnerViewsFlag(): boolean {
return !!window[EnsureDisposeInnerViewsFlagKey];
}

export function setEnsureDisposeInnerViewsFlag(ensureDisposeInnerViews: boolean): void {
window[EnsureDisposeInnerViewsFlagKey] = ensureDisposeInnerViews;
}
44 changes: 26 additions & 18 deletions ReactViewResources/Loader/Internal/ViewPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,49 @@ import { webViewRootId } from "../Internal/Environment";
import { getStylesheets } from "./Common";
import { ViewMetadata } from "./ViewMetadata";
import { ViewSharedContext } from "../Public/ViewSharedContext";
import {addView, deleteView} from "./ViewsCollection";
import {notifyViewDestroyed, notifyViewInitialized} from "./NativeAPI";
import {handleError} from "./ErrorHandler";

export type ViewLifecycleEventHandler = (view: ViewMetadata) => void;
export type ViewErrorHandler = (view: ViewMetadata, error: Error) => void;

export interface IViewPortalProps {
view: ViewMetadata
viewMounted: ViewLifecycleEventHandler;
viewUnmounted: ViewLifecycleEventHandler;
viewErrorRaised: ViewErrorHandler;
shadowRoot: Element;
}

interface IViewPortalState {
component: React.ReactElement;
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line break to remove

function onChildViewAdded(childView: ViewMetadata) {
addView(childView.name, childView);
notifyViewInitialized(childView.name);
}

function onChildViewRemoved(childView: ViewMetadata) {
deleteView(childView.name);
notifyViewDestroyed(childView.name);
}

function onChildViewErrorRaised(childView: ViewMetadata, error: Error) {
handleError(error, childView);
}

/**
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
*
* A View Frame notifies its sibling view collection when a new instance is mounted.
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
* A view portal is persisted until its View Frame counterpart disappears.
* */
*/
export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalState> {

private head: Element;
private shadowRoot: HTMLElement;
private head: HTMLElement;

constructor(props: IViewPortalProps, context: any) {
super(props, context);

this.state = { component: null! };

this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;

props.view.renderHandler = component => this.renderPortal(component);
}

Expand Down Expand Up @@ -67,16 +75,16 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true");
stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true)));

this.props.viewMounted(this.props.view);
onChildViewAdded(this.props.view);
}

public componentWillUnmount() {
this.props.viewUnmounted(this.props.view);
onChildViewRemoved(this.props.view);
}

public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// execute error handling inside promise, to avoid the error handler to rethrow exception inside componentDidCatch
Promise.resolve(null).then(() => this.props.viewErrorRaised(this.props.view, error));
Promise.resolve(null).then(() => onChildViewErrorRaised(this.props.view, error));
}

public render(): React.ReactNode {
Expand All @@ -90,6 +98,6 @@ export class ViewPortal extends React.Component<IViewPortalProps, IViewPortalSta
</div>
</body>
</>,
this.shadowRoot);
this.props.shadowRoot);
}
}
}
95 changes: 95 additions & 0 deletions ReactViewResources/Loader/Internal/ViewPortalLegacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as React from "react";
import { webViewRootId } from "../Internal/Environment";
import { getStylesheets } from "./Common";
import { ViewMetadata } from "./ViewMetadata";
import { ViewSharedContext } from "../Public/ViewSharedContext";

export type ViewLifecycleEventHandler = (view: ViewMetadata) => void;
export type ViewErrorHandler = (view: ViewMetadata, error: Error) => void;

export interface IViewPortalProps {
view: ViewMetadata
viewMounted: ViewLifecycleEventHandler;
viewUnmounted: ViewLifecycleEventHandler;
viewErrorRaised: ViewErrorHandler;
}

interface IViewPortalState {
component: React.ReactElement;
}

/**
* A ViewPortal is were a view is rendered. The view DOM is then moved into the appropriate placeholder.
* This way we avoid a view being recreated (and losing state) when its ViewFrame is moved in the tree.
*
* A View Frame notifies its sibling view collection when a new instance is mounted.
* Upon mount, a View Portal is created and it will be responsible for rendering its view component in the shadow dom.
* A view portal is persisted until its View Frame counterpart disappears.
* */
export class ViewPortalLegacy extends React.Component<IViewPortalProps, IViewPortalState> {

private head: Element;
private shadowRoot: HTMLElement;

constructor(props: IViewPortalProps, context: any) {
super(props, context);

this.state = { component: null! };

this.shadowRoot = props.view.placeholder.attachShadow({ mode: "open" }).getRootNode() as HTMLElement;

props.view.renderHandler = component => this.renderPortal(component);
}

private renderPortal(component: React.ReactElement) {
const wrappedComponent = (
<ViewSharedContext.Provider value={this.props.view.context}>
{component}
</ViewSharedContext.Provider>
);
return new Promise<void>(resolve => this.setState({ component: wrappedComponent }, resolve));
}

public shouldComponentUpdate(nextProps: IViewPortalProps, nextState: IViewPortalState) {
// only update if the component was set (once)
return this.state.component === null && nextState.component !== this.state.component;
}

public componentDidMount() {
this.props.view.head = this.head;

const styleResets = document.createElement("style");
styleResets.innerHTML = ":host { all: initial; display: block; }";

this.head.appendChild(styleResets);

// get sticky stylesheets
const stylesheets = getStylesheets(document.head).filter(s => s.dataset.sticky === "true");
stylesheets.forEach(s => this.head.appendChild(document.importNode(s, true)));

this.props.viewMounted(this.props.view);
}

public componentWillUnmount() {
this.props.viewUnmounted(this.props.view);
}

public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// execute error handling inside promise, to avoid the error handler to rethrow exception inside componentDidCatch
Promise.resolve(null).then(() => this.props.viewErrorRaised(this.props.view, error));
}

public render(): React.ReactNode {
return ReactDOM.createPortal(
<>
<head ref={e => this.head = e!}>
</head>
<body>
<div id={webViewRootId} ref={e => this.props.view.root = e!}>
{this.state.component ? this.state.component : null}
</div>
</body>
</>,
this.shadowRoot);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import * as React from "react";
import { ObservableListCollection } from "./ObservableCollection";
import { ViewMetadata } from "./ViewMetadata";
import { ViewPortal, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";
import { ViewPortalLegacy, ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortalLegacy";
export { ViewLifecycleEventHandler, ViewErrorHandler } from "./ViewPortal";

interface IViewPortalsCollectionProps {
Expand All @@ -15,7 +15,7 @@ interface IViewPortalsCollectionProps {
* Handles notifications from the views collection. Whenever a view is added or removed
* the corresponding ViewPortal is added or removed
* */
export class ViewPortalsCollection extends React.Component<IViewPortalsCollectionProps> {
export class ViewPortalsCollectionLegacy extends React.Component<IViewPortalsCollectionProps> {

constructor(props: IViewPortalsCollectionProps, context: any) {
super(props, context);
Expand All @@ -28,7 +28,7 @@ export class ViewPortalsCollection extends React.Component<IViewPortalsCollectio

private renderViewPortal(view: ViewMetadata) {
return (
<ViewPortal key={view.name}
<ViewPortalLegacy key={view.name}
view={view}
viewMounted={this.props.viewAdded}
viewUnmounted={this.props.viewRemoved}
Expand Down
9 changes: 8 additions & 1 deletion ReactViewResources/Loader/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Task } from "./Internal/Task";
import { ViewMetadata } from "./Internal/ViewMetadata";
import { createPropertiesProxy } from "./Internal/ViewPropertiesProxy";
import { addView, getView, tryGetView } from "./Internal/ViewsCollection";
import { setEnsureDisposeInnerViewsFlag } from "./Internal/ViewMetadataContext";

export { disableMouseInteractions, enableMouseInteractions } from "./Internal/InputManager";
export { showErrorMessage } from "./Internal/MessagesProvider";
Expand Down Expand Up @@ -126,7 +127,8 @@ export function loadComponent(
hasPlugins: boolean,
componentNativeObject: any,
frameName: string,
componentHash: string): void {
componentHash: string,
ensureDisposeInnerViews: boolean): void {

async function innerLoad() {
let view: ViewMetadata;
Expand All @@ -135,6 +137,11 @@ export function loadComponent(
// wait for the stylesheet to load before first render
await defaultStylesheetLoadTask.promise;
}

if (frameName === mainFrameName) {
console.log(`Set ensureDisposeInnerViewsFlag to ${ensureDisposeInnerViews} for main frame`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be removed?

setEnsureDisposeInnerViewsFlag(ensureDisposeInnerViews);
}

view = tryGetView(frameName)!;
if (!view) {
Expand Down
Loading
Loading