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!
Related
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.
i used this.outletService.add('BottomHeaderSlot', factory, OutletPosition.BEFORE); during the search button click handler to add a custom component in the BottomHeaderSlot. I intended to add searchOverlay under the header to add customized search behavior.
But my custom component is not shown under the header after calling outletService.add. I refered to this https://sap.github.io/cloud-commerce-spartacus-storefront-docs/outlets/ . Does outletService support dynamic adding component during runtime?
Following is my button handler
open(): void {
const factory = this.componentFactoryResolver.resolveComponentFactory<SearchOverlayComponent>(SearchOverlayComponent);
this.outletService.add('BottomHeaderSlot', <any>factory, OutletPosition.BEFORE);
this.cd.markForCheck();
That's a good question. At the moment it is a not a feature supported from our outlets.
A solution you could do is inject the component in a more static manner (either CMS or outlet when the app initializes like seen here https://github.com/SAP/cloud-commerce-spartacus-storefront/blob/develop/projects/storefrontlib/src/cms-components/asm/services/asm-enabler.service.ts)?
Your component could then be wrapped with an <ng-container *ngIf="open$ | async></ng-container> where open$ is an observable for the state of the search box. That way the component only appears in the dom when the searchbox is open.
The idea of dynamically adding a components through outlets is a good one we will keep in mind. I will open an issue on Github as an improvement.
I have an Aurelia app where a user can click on a button and create a new tab. The tab and its content (a custom element) do not exist on the page before the user clicks the button. I am generating the HTML for the content at runtime (via Javascript) in my view model.
I keep seeing mention of using the template engine's compose or enhance functions, but neither are working for me. I don't know how I would use the <compose> element (in my HTML) since I am creating the element based on the user clicking a button.
My thought was that the button has a click.delegate to a function that does ultimately does something like
const customElement = document.createElement('custom-element');
parentElement.appendChild(customElement);
const view = this.templatingEngine.enhance({
element : customElement,
container : this.container, // injected
resources : this.viewResources, // injected
bindingContext: {
attrOne: this.foo,
attrTwo: this.bar,
}
});
view.attached();
But all this does is create an HTML element <custom-element></custom-element> without actually binding any attributes to it.
How can I create a custom element analogous to <custom-element attr-one.bind="foo" attr-two.bind="bar"></custom-element> but via Javascript?
As you pointed out in your own answer, it's the missing resources that caused the issue. One solution is to register it globally. That is not always the desired behavior though, as sometimes you want to lazily load the resources and enhance some lazy piece of HTML. Enhance API accepts an option for the resources that you want to compile the view with. So you can do this:
.enhance({
resources: new ViewResources(myGlobalResources) // alter the view resources here
})
for the view resources, if you want to get it from a particular custom element, you can hook into the created lifecycle and get it, or you can inject the container and retrieve it via container.get(ViewResources)
I found the problem =\ I had to make my custom element a global resource.
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.
Fairly new to developing for Windows 8, I'm working on an app that has a rather flat model. I have looked and looked, but can't seem to find a clear answer on how to set a WinJS page to prevent backward navigation. I have tried digging into the API, but it doesn't say anything on the matter.
The code I'm attempting to use is
WinJS.Navigation.canGoBack = false;
No luck, it keeps complaining about the property being read only, however, there are no setter methods to change it.
Thanks ahead of time,
~Sean
canGoBack does only have a getter (defined in base.js), and it reflects the absence or presence of the backstack; namely nav.history.backstack.
The appearance of the button itself is controlled by the disabled attribute on the associated button DOM object, which in turn is part of a CSS selector controlling visibility. So if you do tinker with the display of the Back button yourself be aware that the navigation plumbing is doing the same.
Setting the backstack explicitly is possible; there's a sample the Navigation and Navigation History Sample that includes restoring a history as well as preventing navigation using beforenavigate, with the following code:
// in ready
WinJS.Navigation.addEventListener("beforenavigate", this.beforenavigate);
//
beforenavigate: function (eventObject) {
// This function gives you a chance to veto navigation. This demonstrates that capability
if (this.shouldPreventNavigation) {
WinJS.log && WinJS.log("Navigation to " + eventObject.detail.location + " was prevented", "sample", "status");
eventObject.preventDefault();
}
},
You can't change canGoBack, but you can disable the button to hide it and free the history stack.
// disabling and hiding backbutton
document.querySelector(".win-backbutton").disabled = true;
// freeing navigation stack
WinJS.Navigation.history.backStack = [];
This will prevent going backward and still allow going forward.
So lots of searching and attempting different methods of disabling the Back Button, finally found a decent solution. It has been adapted from another stackoverflow question.
Original algorithm: How to Get Element By Class in JavaScript?
MY SOLUTION
At the beginning of a fragment page, right as the page definition starts declaring the ready: function, I used an adapted version of the above algorithm and used the resulting element selection to set the disabled attribute.
// Retrieve Generated Back Button
var elems = document.getElementsByTagName('*'), i;
for (i in elems)
{
if((" "+elems[i].className+" ").indexOf("win-backbutton") > -1)
{
var d = elems[i];
}
}
// Disable the back button
d.setAttribute("disabled", "disabled");
The code gets all elements from the page's DOM and filters it for the generated back button. When the proper element is found, it is assigned to a variable and from there we can set the disabled property.
I couldn't find a lot of documentation on working around the default navigation in a WinJS Navigation app, so here are some methods that failed (for reference purposes):
Getting the element by class and setting | May have failed from doing it wrong, as I have little experience with HTML and javascript.
Using the above method, but setting the attribute within the for loop breaks the app and causes it to freeze for unknown reasons.
Setting the attribute in the default.js before the navigation is finished. | The javascript calls would fail to recognize either methods called or DOM elements, presumably due to initialization state of the page.
There were a few others, but I think there must be a better way to go about retrieving the element after a page loads. If anyone can enlighten me, I would be most grateful.
~Sean R.