Blazor Server, Conditional Elements + JS Invoking - asp.net-core

I have a Blazor Server app that is a multi-step "wizard" form. After each relevant step the state is adjusted, and new HTML is shown/hidden via conditional statements (simple example below).
if (IsStepSignature)
{
<div>Signature HTML here</div>
}
This all works just fine. My problem comes when I need to invoke some JS logic on the dynamically generated HTML from above (e.g. click handlers to hook up external JS libraries). When I handle the "Next" click, I can invoke the JS just fine...but it is not yet seeing the dynamic HTML from above. Is there a way to invoke some JS, and control it so that it doesn't execute until after the page is redrawn from the C# code execution?
5/18/2020 Update from Nik P
Can leverage some flags and use OnAfterRenderAsync to control this ordering. This does work, but it does require some extra hops to/from the server. Below is what I see when implementing this. This may just be the nature of Blazor Server, as one of the pros/cons is some known added chattiness. In total these requests were 2.5K, so extremely small.
CLIENT --> Browser Dispatch Event (NEXT CLICK)
Render <-- SERVER
CLIENT --> Render Complete
Invoke JS <-- SERVER
CLIENT --> Invoke Complete

The issue you are having has to do with the element not existing in the client side HTML at all until after the re-render takes place. So one way to do this is to set a boolean flag in your C# code that says there is code that needs to be run after the render, and populate support fields that you will need for your JS Interop. Whenever you need to run the JS interop, set your flag to true, set your support fields to the values you need for the JS interop call, and then do something that kicks a DOM diff calculation. (Even StateHasChanged should be enough, but adding your items conditionally as you mentioned will also do it) Then, override your OnAfterRenderAsync method as follows:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
// any first render code
}
if(yourFlag)
{
YourJSInteropMethod(supportField1, supportfield2);
yourflag = false;
}
}
The simplicity in this approach is that the DOM update will always happen ahead of the OnAfterRenderAsync call, so your HTML will be populated with what you are targeting with JS.

Related

How to troubleshoot a slow re-render in Blazor; how to get the render time of each component?

I have a Blazor WebAssembly site, and the component tree has grown quite a bit.
In my Home component, a change in a child component "Foo" can happen that causes parameter property "IsChecked" to change; one of these properties is passed into a different child component "Bar". So therefore, in my Home component, Foo's EventCallback IsCheckedChanged invokes StateHasChanged() so that the Home component and therefore all its children re-render.
This can be slow...for me, it is taking 1-2 seconds to render and paint when even only one boolean property changes. Supposing the user is checking and unchecking several checkboxes to trigger this rerender: this delay would be quite unwelcome on each one. (I have already ruled out that any of my other code in Home could be slowing things down.) So I gather that I have to figure out which parts of which components are slowest and perhaps have ShouldRender reutrn false in some cases, or otherwise do some optimization therein. My trouble is that I don't see which components are most contributing to the render time.
So is there any way to see or write out the time it takes a Blazor component to render, as "render" is described in the lifecycle document? I assume any such render time would include the render time of all children, of course.
Is there any better way to profile Blazor WebAssembly render times?
(1) Rendering from C# code to HTML's DOM is low-level process. Blazor debugging process only allow see value of objects/values. No out-of-the-box for tracing Blazor redering process (generate DOM from C# code). You can see trick
(2) You can use DOM change tracing:
https://dev.to/apvarun/chrome-devtools-detecting-element-changes-gh6
https://developers.google.com/web/updates/2012/02/Detect-DOM-changes-with-Mutation-Observers
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
(3) A trick use counter put inside per component by using a Blazor BaseComponent. https://www.codeproject.com/Articles/5290129/Exploring-Blazor-Component-Rendering
https://allinoneserver.azurewebsites.net/wasm.html
use BaseCounter
#inherits BaseCounter
// Markup the same as BaseCounter
#code {
protected override string buttoncolor => "btn-success";
protected override Task OnInitializedAsync()
{
Service.CounterChanged += ReRender;
return base.OnInitializedAsync();
}
protected void ReRender(object sender, EventArgs e) =>
this.InvokeAsync(this.StateHasChanged);
}
and components extend from the BaseCounter, when per component called, counter will increase value, it help you see what is rendering and order of rendering process.
(4) Outside the question, for improving performance, see Optimize rendering speed at https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-performance-best-practices?view=aspnetcore-5.0#optimize-rendering-speed

Hide or modify the toolbar of Webview2 when viewing pdf

I am using the new Webview2 control to render Pdf files in my WPF application.
This is working well but I would like to customize the toolbar to hide for example the save buttons on some criteria. I did not find methods or properties to do that directly from the Webview2/CoreWebView2 object.
But if I inspect the web page code generated when the pdf is rendered I can see the part where the save buttons are
Is it possible to intercept the whole page before it is rendered and alter the html ? I know it is dirty to do this as it will require to look for the class Id that is subject to change but it will work for now as a temporary solution.
Yes it is 100% possible to add scripts into your environment, that can intercept and control the content you are rendering.
I'm using WebView2 in windows forms, so I'll show you how I do it in a desktop windows forms application, the process will be similar for other environments, and is almost identical for WPF projects.
Step 1
Setup and initialise the default settings for your webview2 control
public FrmMainForm()
{
InitializeComponent();
// Resize/position client to a standard 720p HD TV and make room for tools
var toolBarSize =
PnlToolPanelTopRow.Height +
PnlToolPanelLowerRow.Height;
SetClientSizeCore(1280, toolBarSize + 720);
//webview.Top = toolBarSize;
webview.Height = 720;
// Webview initialisation handler, called when control instatiated and ready
webview.CoreWebView2InitializationCompleted += Webview_CoreWebView2InitializationCompleted;
// Set defaults for tools
TxtAppUrl.Text = "http://app/";
TxtUserAgent.Text = DEFAULTUA;
}
Note the "initialization handler" attached to the Initialization completed event.
Step 2
In your initialization event handler, you need to load in the JavaScripts you wish to run every time a browser view is initialised. These scripts are ALWAYS executed before any of the scripts in the loaded page, or before the dom has loaded, they are executed after the page navigation however, so you cannot use them to control the navigation cycle.
Here's an example, from one of my projects
private void Webview_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
// Custom URL handler (All URLS starting "http://app/" are intercepted directly by the application
webview.CoreWebView2.AddWebResourceRequestedFilter("http://app/*", CoreWebView2WebResourceContext.All);
webview.CoreWebView2.WebResourceRequested += WebResourceRequested;
// Load in our custom JS API files to create the HbbTv JS environment
webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(JsLoader.LoadApi("BrowserOverrides.js"));
// Assign any back end C sharp classes to support our JS api's
webview.CoreWebView2.AddHostObjectToScript("channelData", new ChannelData());
// Event handler for notification of dom content load finished
//webview.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
// Show dev tools by default
webview.CoreWebView2.OpenDevToolsWindow();
// Other misc settings
webview.CoreWebView2.Settings.UserAgent = DEFAULTUA;
}
The line you need to pay attention too is the one that reads:
webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(JsLoader.LoadApi("BrowserOverrides.js"));
I use a custom loader class, that grabs my java script file from resources compiled into my application, but you however do not need to, the one and only parameter that is passed to the AddScript call is just a plain string of java script code, for example:
webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("alert('hello world');");
Will cause your webview2 session to display an alert after every navigation, but before the dom & scripts in the page are loaded and run, for every single page browsed to in the webview.
The JS code has full access to the browser environment, so it's simply a case of attaching a regular JS handler to one of the "dom loaded" or "page loaded" regular java script events, then using standard dom manipulation to find the elements you want and setting their display style to "none".
Step 3 (optional)
You'll see also from my second code snippet, that there are 2 further hook points you can use.
webview.CoreWebView2.AddHostObjectToScript("channelData", new ChannelData());
Adds my custom C# class type "ChannelData" to the browser environment, this is then made available to the browser via the following JS code
chrome.webview.hostObjects.channelData.(method name or property name accessible here)
The "channelData" name, after "hostObjects" is the same as used in the first parameter of the "AddHostObject" call, the second news up a regular C# class which MUST be set up as follows:
using HbbTvBrowser.DataClasses;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
namespace JsSupportClasses
{
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class (Class Name Here)
{
public string SomeMethod()
{
return JsonConvert.SerializeObject(Some C# objects to return);
}
}
}
If the class interface and com visible attributes are not provided the class will not be seen by web view and will not be callable.
Any data returned by your methods back to JS code, MUST be a simple data type such as int, bool or string, this means that you cannot return complex objects without first serialising them, then de-serialising them once your back in JS land.
You'll note that there is also a C# level dom ready handler. This is fired as as soon as the HTML is loaded and ready to be accessed, but before images, scripts, css or anything else like that is loaded. You cannot however alter the dom content from C#, at least not at the moment anyway, I do believe it may be on the teams radar for sometime this year, but I cannot confirm that.
While I have not had need to do so my self, I do believe however it is possible to obtain a copy in C# of the loaded HTML, but it is just that "A copy", altering it does not alter the actual loaded dom.
WebView2 now offers a HiddenPdfToolbarItems setting. Here's the code I used in the code behind:
using Microsoft.Web.WebView2.Core;
[...]
InitializeComponent();
webView.CoreWebView2InitializationCompleted += (sender, e) =>
{
if (e.IsSuccess)
{
webView.CoreWebView2.Settings.HiddenPdfToolbarItems =
CoreWebView2PdfToolbarItems.Bookmarks
| CoreWebView2PdfToolbarItems.FitPage
| CoreWebView2PdfToolbarItems.PageLayout
| CoreWebView2PdfToolbarItems.PageSelector
| CoreWebView2PdfToolbarItems.Print
| CoreWebView2PdfToolbarItems.Rotate
| CoreWebView2PdfToolbarItems.Save
| CoreWebView2PdfToolbarItems.SaveAs
| CoreWebView2PdfToolbarItems.Search
| CoreWebView2PdfToolbarItems.ZoomIn
| CoreWebView2PdfToolbarItems.ZoomOut;
}
};
Replace webView with the name of your WebView2 control and remove the names of any Toolbar Items you still need. Hope that helps!

Server Side Rendering Vue with ASP.NET Core 2

I'm trying to understand the usage and limitations of server side rendering with vuejs when using aspnet core.
I used this starter kit for aspnet core and vuejs to setup a simple vue site, which is running based on the code here: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/master
I then modified the project to update the aspnet-prerendering and added vue-server-renderer, compiling a hodgepodge of sources to cobble together this update: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/ssr
If I run this project, the site appears to load fine, and if I turn off the javascript in the browser, I can see that it does appear that the server-side rendering executed and populated the html result:
however, because JavaScript is disabled, the content isn't moved into the dom as it looks like it is trying to...
My understanding of server-side rendering is that it would populate the html entirely and serve a completed page to the user, so that even if JS was disabled, they'd at least be able to see the page (specifically for SEO purposes). Am I incorrect?
Now I believe modern search engines will execute simple scripts like this to get the content, but I still don't want a blank page rendered if js is disabled...
Is this a limitation of server-side rendering, or perhaps specifically ssr with vue and/or aspnet core?
or am I just missing a step somewhere?
Edit: more information
I looked at the source code for what I believe is the method that prerenders the section here: https://github.com/aspnet/JavaScriptServices/blob/dev/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs
The line
output.Content.SetHtmlContent(result.Html);
has a null value for result.Html. However, when I manually edit this value to put a test value, it also doesn't render to the output html, and the app div tag is still empty...
If I'm doing something wrong to populate the result.Html value with the expected output, that's one thing, and I would appreciate some help in doing that, especially since the output html appears to be found, since it's in the script that immediately follows...
However, even if I were to populate it, it appears it's being skipped, as evidenced by me manually changing the value. is this a bug in the code or am I doing somethigng wrong, or perhaps both?
As you correctly noticed, for your project, result.Html inside the tag helper is null. So that line cannot be the location where the output is being generated. Since the HTML output from your prerendering script also does not include a script tag, it is clear that something has to generate that. The only other line that could possible do this is the following from the PrerenderTagHelper:
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
That would fit the observed output, so we should figure out where the globalsScript comes from.
If you look at the PrerenderTagHelper implementation, you can see that it will call Prerenderer.RenderToString which returns a RenderToStringResult. This result object is deserialized from JSON after calling your Node script.
So there are two properties of interest here: Html, and Globals. The former is responsible for containing the HTML output that finally gets rendered inside the tag helper. The latter is a JSON object containing additional global variables that should be set for the client side. These are what will be rendered inside that script tag.
If you look at the rendered HTML from your project, you can see that there are two globals: window.html and window.__INITIAL_STATE__. So these two are set somewhere in your code, although html shouldn’t be a global.
The culprit is the renderOnServer.js file:
vue_renderer.renderToString(context, (err, _html) => {
if (err) { reject(err.message) }
resolve({
globals: {
html: _html,
__INITIAL_STATE__: context.state
}
})
})
As you can see, this will resolve the result containing just a globals object with both html and __INITIAL_STATE__ properties. That’s what gets rendered inside of the script tag.
But what you want to do instead is have html not as part of globals but on the layer above, so that it gets deserialized into the RenderToStringResult.Html property:
resolve({
html: _html,
globals: {
__INITIAL_STATE__: context.state
}
})
If you do it like that, your project will properly perform server-side rendering, without requiring JavaScript for the initial view.

JavaScript from a Web page to a Safari extension

Can a Web page communicate directly with a Safari extension on JavaScript level?
Ideally, I'd like some way to invoke the JavaScript in the extension's global page, but talking to the injected script will do, too, since that can talk to the global page via messages.
Direct function calling doesn't work. The window object the page and the injected script have are distinct. safari.self is not available to a regular Web page. Is there dispatchMessage in some other DOM object that's visible to the Web page code?
Found one clumsy workaround. The injected script and the page share the document object. The injected script would create an HTML element in the page (an <input type="hidden"> in my case), give it an agreed-upon ID, and hook its onclick. The page would find that element by ID and invoke onclick, passing some arguments along. The injected script gets control. For script-to-page callbacks, I'd use onchange on the same element.
The calls are synchronous, even.
The injected script would then pass the message on to the extension's global page using safari.self.tab.dispatchMessage().

Is it possible to HIDE Javascript Object's prototype! What's the MYSTERY behind this?

I'm using openui5. There is a constructor Function for UI control Button,unable to see the prototype properties of the Button but the same thing when executed in browser console, shows up!
sap.m.Button.prototype.Move = function(){
console.log('Move');
}
var oButton = new sap.m.Button({text:"Hello"});
oButton.Move(); // throws undefined function!
The same code when executed browser in console, it works!
jsbin --> http://jsbin.com/tepum/1/edit
After running the code I find that creating the first instance of sap.m.Button causes script to change the prototype of sap.m.Button. It's valid in JavaScript but not very smart if you ask me.
A first creation causes a synchronous request (no no as well) to fetch library-parameters.json.
If you run the code the second time it will have prototype.move because creating an instance of Button will not change the Button.prototype.
The capital M in Move would suggest a constructor function so I would advice changing it to lower case.
Since fetching the parameters is synchronous you can create the first instance and then set the prototype:
console.log("First Button creation changes Button.prototype");
var oButton = new sap.m.Button({text:"Hello"});
sap.m.Button.prototype.move = function(){
console.log('Move');
}
oButton.placeAt('content');
oButton.move(); // logs Move
My guess is that this is done to lazy load controls, if a Button is never created then the json config files are never loaded for these unused controls. It has a couple of drawbacks though.
You have to create an instance first before you can set the prototype.
The config files are synchronously loaded so when creating first instance of many controls with a slow connection would cause the app to be unresponsive.
A better way would be for a factory function to return a promise so you create the control the same way every time and the config files can be fetched asynchronously.
[update]
Looking at the config it seems to be config for the whole gui library so I can't see any reason why this is loaded only after creating a first instance. A library that changes it's object definitions when creating instances is not very easy to extend because it's unpredictable. If it only changes prototype on first creation then it should be fine but it looks like the makers of the library didn't want people to extend it or they would not make the object definition unpredictable. If there is an api documentation available then maybe try to check that.
[update]
It seems the "correct" way to extend controls is to use extend.
#HMR is right the correct way to extend a control is by using the extend function provided by UI5 managed objects, see http://jsbin.com/linob/1/edit
in the example below when debugging as mentoned by others you will notice that the control is lazy loaded when required, any changes you make prior are lost when loaded
jQuery.sap.declare("my.Button");
jQuery.sap.require("sap.m.Button");
sap.m.Button.extend("my.Button", {
renderer: {}
});
my.Button.prototype.Move = function() {
console.log('Move');
};
var oButton = new my.Button({
text: "Hello"
});
oButton.placeAt('content');
oButton.Move();
It's not hiding the prototype per se. If a constructor function exits normally then you get that function's prototype. But, if a constructor function actually returns some other object then you get that other object's prototype, so it's not valid to assume that just because you added to the Button prototype that when you call new Button() that you will see your method on whatever you get back. I'm sure if you de-obfuscate that code you'll find that the constructor you are calling has a "return new SomeOtherInstanceOfButton()" or similar at the end of it.
Edit: Ok it's a bit difficult to see what's really going on in that sap code but, it looks like they have code that overwrites the prototypes of controls to add features to them, such as: sap.ui.core.EnabledPropagator, and those things aren't run until you actually instantiate a button. So if you change your code to instantiate the button on the page, then add to it's prototype, then construct and call the method, it works fine. Like so:
http://jsbin.com/benajuko/2/edit
So I guess my answer is, when you run it from console it's finished mucking around with that prototype, whereas in your test you were adding to the prototype, then constructing the button for the first time (which changes the prototype again) then trying to call your old one, which is no longer there.