SetVirtualHostName within WebMessageReceived event handler doesn't work - c#

I'm trying to dynamically create an image within a WebView2, using a source passed in from the WebView2's containing application.
The source image exists on an arbitrary location on the filesystem. To allow the WebView2 to access the image, I'm using SetVirtualHostNameToFolderMapping, which maps a hostname within the webview to some folder.
I want this to happen only after some interaction from inside the webview, using postMessage and the WebMessageReceived event, as described here.
If I call SetVirtualHostNameToFolderMapping outside of the WebMessageReceived handler, even after I've navigated to the HTML using NavigateToString, the webview loads the image.
But if I call SetVirtualHostNameToFolderMapping within the WebMessageReceived handler:
webview.CoreWebView2.WebMessageReceived += (s, e) => {
webview.CoreWebView2.SetVirtualHostNameToFolderMapping(
"assets",
#"C:\path\to\folder",
CoreWebView2HostResourceAccessKind.Allow
);
webview.CoreWebView2.PostWebMessageAsString(#"breakpoint.bmp");
};
the webview can't find the image; it fails with:
Failed to load resource: net::ERR_NAME_NOT_RESOLVED
How can I debug this? Are there any other alternatives I could use to do something similar?
XAML:
<Window x:Class="_testWebviewDialogs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf">
<wv:WebView2 x:Name="webview" />
</Window>
Code-behind:
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Web.WebView2.Core;
namespace _testWebviewDialogs;
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
Loaded += async (s, e) => await initializeAsync();
}
private async Task initializeAsync() {
await webview.EnsureCoreWebView2Async();
webview.CoreWebView2.WebMessageReceived += (s, e) => {
webview.CoreWebView2.SetVirtualHostNameToFolderMapping(
"assets",
#"C:\path\to\folder",
CoreWebView2HostResourceAccessKind.Allow
);
webview.CoreWebView2.PostWebMessageAsString(#"breakpoint.bmp");
};
var html = await File.ReadAllTextAsync("container.html");
webview.NavigateToString(html);
}
}
container.html:
<!DOCTYPE html>
<html>
<body>
<button id="button1">Click me</button>
<img id ="img1" src="" />
<script>
document.getElementById("button1").addEventListener('click', ev => {
chrome.webview.postMessage('source');
});
window.chrome.webview.addEventListener('message', event => {
document.getElementById('img1').src = `https://assets/${event.data}`;
});
</script>
</body>
</html>

I filed an issue for this. Per the response, this behavior is documented:
As the resource loaders for the current page might have already been created and running, changes to the mapping might not be applied to the current page and a reload of the page is needed to apply the new mapping. (source)
But it's possible to create (and recreate) a symbolic link to different target paths, and map the virtual host name to that symbolic link. The virtual host mapping uses the current symlink target.
var symlinkPath =
Path.Combine(
Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location!
)!,
"assets"
);
Directory.CreateDirectory(symlinkPath);
webview.CoreWebView2.SetVirtualHostNameToFolderMapping(
"assets",
symlinkPath,
CoreWebView2HostResourceAccessKind.Allow
);
webview.CoreWebView2.WebMessageReceived += (s, e) => {
Directory.Delete(symlinkPath);
Directory.CreateSymbolicLink(symlinkPath, #"C:\path\to\target\directory");
webview.CoreWebView2.PostWebMessageAsString(#"breakpoint.bmp");
};

Related

How to navigate to a xaml page from ResourceDictionary class (WPF)

I have a ResourceDictionary class as follows:
using System.Windows.Navigation;
using static myApp.filesXAML.Login;
namespace myApp.Clases
{
partial class functionsMenu : ResourceDictionary
{
private void imageCloseMyApp(object sender, MouseButtonEventArgs e)
{
NavigationService.Navigate(new Uri("filesXAML/Login.xaml", UriKind.Relative));
}
}
}
The imageCloseMyApp function is invoked from an image by clicking on it and I wish to call another page.
And I get the following error before compiling the project:
Severity Code Description Project File Line Status deleted Error
CS0120 An object reference is required for the field, method or
property 'NavigationService.Navigate (Uri)' not static myApp
H:\pro\Visual_Studio\myApp\myApp\Classes\ FunctionsMenu.cs 35 Active
I have searched the internet, and tried the following options:
Login page = new Login();
page.NavigationService.Navigate(new Uri("filesXAML/Login.xaml",UriKind.RelativeOrAbsolute));
// or
NavigationService nav = NavigationService.GetNavigationService(Application.Current.Windows[0].Parent);
nav.Navigate(new Uri("filesXAML/Login.xaml", UriKind.Relative));
But none works.
Any suggestions or comments?
I have achieved it as follows:
NavigationService nav = NavigationService.GetNavigationService((Grid)((Image)sender).Parent);
nav.Navigate(new Uri("filesXAML/Login.xaml", UriKind.Relative));
I guide myself to understand the following thread:
How do you get the parent control from an event send by a control in the resources

Scroll to specified part of page when clicking top navigation link in Blazor

How can I make a simple "jump to" part of already loaded page in Blazor? Like this in HTML:
Contact us
...
<section id="contact">
Ideally I also want to have it smooth scroll down to this section. Thought I would try to solve this with CSS, but maybe not possible?
I've solved this by using a button and then writing some inline Javascript in the markup. You can generalize this to a Blazor component for bonus points!
<button type="button" onclick="document.getElementById('contact').scrollIntoView({behavior:'smooth'})">Contact us</button>
...
<section id="contact">
What you need is the hashed routes features of Blazor. But, alas, no such features do exist yet. I'd suggest you use JSIterop to perform this task: Create a JavaScript that performs the navigation, and pass it an ElementRef object.
Hope this helps...
Edit: The following is an adaptation of the best workaround solution I've found in Github...
Ordinarily, when you click the link to contact, you get redirected to the route http://localhost:5000/mypage#contact, but will be at the top of the page. The fragment of the route is not used for selection of a specific HTML element.
The current workaround is to write explicit code that interprets the URL. In the example above, we could use a little bit of JavaScript and then call that from our Blazor code:
mypage.cshtml:
#page "/mypage"
#inject Microsoft.AspNetCore.Components.Services.IUriHelper UriHelper
<nav>
contact
</nav>
<section>
<h2 id="contact">contact</h2>
</section>
#functions {
protected override void OnInit()
{
NavigateToElement();
UriHelper.OnLocationChanged += OnLocationChanges;
}
private void OnLocationChanges(object sender, string location) => NavigateToElement();
private void NavigateToElement()
{
var url = UriHelper.GetAbsoluteUri();
var fragment = new Uri(url).Fragment;
if(string.IsNullOrEmpty(fragment))
{
return;
}
var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;
if(string.IsNullOrEmpty(elementId))
{
return;
}
ScrollToElementId(elementId);
}
private static bool ScrollToElementId(string elementId)
{
return JSRuntime.Current.InvokeAsync<bool>("scrollToElementId", elementId).GetAwaiter().GetResult();
}
}
index.html:
<script>
window.scrollToElementId = (elementId) => {
console.info('scrolling to element', elementId);
var element = document.getElementById(elementId);
if(!element)
{
console.warn('element was not found', elementId);
return false;
}
element.scrollIntoView();
return true;
}
</script>
Note: If you're using Blazor version .9.0, you should inject the IJSRuntime
Please, let me know if this solution works for you...
You can use 2 Nuget packages to solve this:
Scroll JS which is part of JsInterop Nuget. This has many feature see the docs but main part is IScrollHandler which is an injectable service. You can try it out with the demo app. You can see the scrollIntoView() and other JS functions wrapped, smooth scroll available. So much simpler to use JS scroll support...
Second option is to use "Parmalinks" in the URL you have "#". Nuget available here. It is what you requested. Basically it is using <a> tags but you don't have to bother with it (note even demo app has # URLs). Also renders "link" component with clickable icon and navigate/copy actions. Currenlty smooth scroll is not available but can be requested. You can try it out with the demo app.
I used Arron Hudon's answer and it still didn't work. However, after playing around I realized it wouldn't work with an anchor element: <a id='star_wars'>Place to jump to</a>. Apparently Blazor and other spa frameworks have issues jumping to anchors on the same page. To get around that I had to use a paragraph element instead (section would work too): <p id='star_wars'>Some paragraph<p>.
Example using bootstrap:
<button class="btn btn-link" onclick="document.getElementById('star_wars').scrollIntoView({behavior:'smooth'})">Star Wars</button>
... lots of other text
<p id="star_wars">Star Wars is an American epic...</p>
Notice I used bootstrap's btn-link class to make the button look like a hyperlink.
The same answer as the Issac's answer, but need to change some code.
I found the main problem is that you need it to be async. #johajan
#inject IJSRuntime JSRuntime
...
#functions {
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
//NavigateToElement();
UriHelper.OnLocationChanged += OnLocationChanges;
}
private async Task OnLocationChanges(object sender, string location) => await NavigateToElement();
private async Task NavigateToElement()
{
var url = UriHelper.GetAbsoluteUri();
var fragment = new Uri(url).Fragment;
if(string.IsNullOrEmpty(fragment))
{
return;
}
var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;
if(string.IsNullOrEmpty(elementId))
{
return;
}
await ScrollToElementId(elementId);
}
private Task<bool> ScrollToElementId(string elementId)
{
return JSRuntime.InvokeAsync<bool>("scrollToElementId", elementId);
}
}

Calling Dynamically Created Textboxes in C# Visual Studio

In this post I wanted to figure out how to create dynamically created textboxes in C# Visual Studio.
Adding additional textboxes to aspx based on xml
However, when I try to call the ID of these dynamically created textboxes later in my code to figure out what text the user entered into them, I am getting an error that says these IDs do not exist in the current context. Does anyone know how I would be able to call these?
credit to Adding additional textboxes to aspx based on xml
Here is my entire code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml.Linq;
namespace WebApplication4
{
public partial class WebForm15 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsCallback)
{
//credit to https://stackoverflow.com/questions/44076955/adding-additional-textboxes-to-aspx-based-on-xml#comment75336978_44078684
const string xml = #"<Number>
<Num>1</Num>
<Num>2</Num>
<Num>3</Num>
</Number>";
XDocument doc = XDocument.Parse(xml);
int i = 0;
foreach (XElement num in doc.Root.Elements())
{
TextBox box = new TextBox
{
ID = "dynamicTextBox" + i,
Text = num.Value,
ReadOnly = false
};
divToAddTo.Controls.Add(box);
divToAddTo.Controls.Add(new LiteralControl("<br/>"));
i++;
}
}
}
protected void BtnGetValues_Click(object sender, EventArgs e)
{
IList<string> valueReturnArray = new List<string>();
foreach (Control d in divToAddTo.Controls)
{
if (d is TextBox)
{
valueReturnArray.Add(((TextBox)d).Text);
}
}
//valueReturnArray will now contain the values of all the textboxes
}
}
}
Here is aspx:
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm15.aspx.cs" Inherits="WebApplication4.WebForm15" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
</head>
<body>
<form id="form1" runat="server">
<div id="divToAddTo" runat="server" />
<asp:Button runat="server" ID="BtnGetValues" Text="GetValues" OnClick="BtnGetValues_Click" />
</form>
</body>
</html>
Figured it out!!! Here is what I found after scouring the internet for hours
Solution:
When using dynamic controls, you must remember that they will exist only until the next postback.ASP.NET will not re-create a dynamically added control. If you need to re-create a control multiple times, you should perform the control creation in the PageLoad event handler ( As currently you are just creating only for first time the TextBox using Condition: !IsPostabck ). This has the additional benefit of allowing you to use view state with your dynamic control. Even though view state is normally restored before the Page.Load event, if you create a control in the handler for the PageLoad event, ASP.NET will apply any view state information that it has after the PageLoad event handler ends.
So, Remove the Condition: !IsPostback, So that each time the page Loads, The TextBox control is also created. You will also see the State of Text box saved after PageLoad handler completes. [ Obviously you have not disabled ViewState!!! ]
Example:
protected void Page_Load(object sender, EventArgs e)
{
TextBox txtBox = new TextBox();
// Assign some text and an ID so you can retrieve it later.
txtBox.ID = "newButton";
PlaceHolder1.Controls.Add(txtBox);
}
Now after running it, type anything in text box and see what happens when you click any button that causes postback. The Text Box still has maintained its State!!!

Print webbrowser content as landscape

I need to print document from WebBrowser control as landscape (without showing PrintPreview and changing default printer page orientation).
I've tried:
TemplatePrinter
Here's my template template.html:
<HTML XMLNS:IE>
<HEAD>
<?IMPORT NAMESPACE="IE" IMPLEMENTATION="#default">
<TITLE>Landscape</TITLE>
</HEAD>
<BODY>
<IE:TEMPLATEPRINTER id="Printer"/>
<SCRIPT Language="JavaScript">
Printer.orientation="landscape";
</SCRIPT>
</BODY>
</HTML>
And I try to use it:
mshtml.IHTMLDocument2 doc = wbReport.Document.DomDocument as mshtml.IHTMLDocument2;
doc.execCommand("print", false, "template.tpl");
Create reg key "orientation" with value "2" in HKCU\Software\Microsoft\Internet Explorer\PageSetup
Add style like:
<style type="text/css" media="print">
#page
{
size: landscape;
margin: 2cm;
}
</style>
But I still print my html as portrait.
Maybe there is better solution to print html as landscape or I have some mistakes when using TemplatePrinter?
After messing around with the registry, attempting to use print templates in IE and various other methods (none of which worked) i've produced a somewhat hacky workaround.
I've extended the WebBrowser control and created a new print preview method inside which I have methods to pass the ALT+L key combination to enable landscape view:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace YourProjectNamespace
{
class ExWebBrowser : WebBrowser
{
void setLandscapeTimer_Tick(object sender, EventArgs e)
{
SendKeys.SendWait("%L");
this.ProcessDialogKey(Keys.Alt & Keys.L);
SendKeys.Flush();
}
public void _ShowPrintPreviewDialog()
{
this.ShowPrintPreviewDialog();
this.Focus();
Timer setLandscapeTimer = new Timer();
setLandscapeTimer.Interval = 500;
setLandscapeTimer.Tick += new EventHandler(setLandscapeTimer_Tick);
setLandscapeTimer.Start();
}
}
}
All you need to do is simply instantiate the object, load a document as with the standard WebBrowser and call _ShowPrintPreviewDialog.
Note you may want to stop the timer once the landscape view has been set.
HTH somebody with the same issue.

How to send an xml serialized object from ASP.NET to Silverlight

On the button click on an ASP.NET page, I need to load a silverlight application, passing a serialized object from ASP.NET codebehind to MainPage.xaml.cs. How to do this?
Why not use WCF? This is a perfect fit for sending serialized objects. Also, WCF hosts well on IIS, so it works great with ASP. Here is a tutorial to get you started. You should be able to see clearly how to define a simply API that you can call from Silverlight. You just need to make your object part of a DataContract.
Do either of these help - http://www.silverlight.net/archives/videos/using-startup-parameters-with-silverlight or http://forums.silverlight.net/t/183963.aspx/1 ?
Some options for you, may help. You could use Javascript - silverlight.net on scripting Silverlight to reach inside the Silverlight object from your page.
Another option is to have the Silverlight object access the AspNet page to ask for it's xml using PageMethods. ( System.Web.Services.WebMethod) once it's loaded.
One option is to configure a Silverlight onLoad event in the <object> tag for your app:
<param name="onLoad" value="setInfo" />
Then use script to push the XML into your app (dynamically insert the XML onto the page from ASP.NET):
<script type="text/javascript">
function setInfo(sender) {
var msg = '<yourtag>your info here</yourtag>';
sender.getHost().content.Page.SetInfo(msg);
}
</script>
To allow script to call your app, configure as follows:
public MainPage()
{
HtmlPage.RegisterScriptableObject("Page", this);
InitializeComponent();
}
[ScriptableMember]
public void SetInfo(string xml)
{
// do stuff
}
Register onLoad param in your Silverlight <Object> tag:
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2">
<param name="source" value="ClientBin/MySlApp.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="4.0.50826.0" />
<param name="autoUpgrade" value="true" />
<param name="onLoad" value="onSilverlightLoad" />
</object>
and register <Script>
<script type="text/javascript">
function onSilverlightLoad(sender, args)
{
var mySlControl = sender.getHost();
mySlControl.content.Communicator.MyDeserializer("SerializedObjectString")
}
</script>
In your Silverlight app register `Communicator' object so it is the app itself:
namespace MySilverlightApp
{
public MySilverlightApp()
{
InitializeComponent();
HtmlPage.RegisterScriptableObject("Communicator", this);
}
}
and create de-serialization function decorated with [ScriptableMember]:
[ScriptableMember]
public void MyDeserializer(string _stringPassedFromHtmlDocument)
{
//deserialize _stringPassedFromHtmlDocument
}
I have above working in one of Sharepoint projects utilising Silverlight webpart. Serialized object is however rendered into HTML so not sure if that will work for your Button.Click() requirement. One thing though should you go down this route: I encountered many many many issues when trying XML serialization and found JSON to be better alternative.
In your MainPage.xaml.cs, define a property getter/setter for whatever object type you need passed.
In your ASP.NET page button click handler, set the property to the serialized object.
If you need this to maintain the serialized object after the page lifecycle finishes, simply change the property setter in MainPage.xaml.cs to persist the serialized object across page lifecycles.
Hope this helps.
Pete
There are two possible paths here:
1) When the button is pressed, the page posts back to the server, gathers some information, serializes it into XML, shows the silverlight component, which then loads the serialized XML.
2) When the page is loaded, the XML data is available. Pressing the button simply shows the silverlight component and asks it to load the XML data.
Scenario 1
Here are the steps that you need to take for scenario 1.
1) Add the silverlight component to the page and embed it in a container (div, table, whatever you like) that is set to runat server. Note that we also specify an onload param to fire a specific event when the silverlight object has finished loading:
<div id="divDiagram" runat="server">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
id="objDiagram">
<param name="onLoad" value="RefreshDiagram" />
</object>
</div>
2) In your code-behind, hide this container until the button is pressed. For example:
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
divDiagram.Visible = false;
}
}
void Button1_Click(object sender, EventArgs e)
{
divDiagram.Visible = true;
}
3) Add a hidden input to hold the serialized data:
<input type="hidden" id="txtSerializedData" runat="server" />
4) Set the contents of this input on the button click:
txtSerializedData.Value = "some serialized data";
5) Modify the silverlight components code to expose the control to javascript:
public MainPage()
{
InitializeComponent();
System.Windows.Browser.HtmlPage.RegisterScriptableObject("DiagramPage", this);
}
6) Add a method to the silverlight control that can be called from javascript (this is the ScriptableMember attribute) to get the serializable content and work with it:
[System.Windows.Browser.ScriptableMember()]
public void RefreshDiagram()
{
// Fetch the hidden input control from the page
var serializedElement = System.Windows.Browser.HtmlPage.Document.GetElementById("txtSerializedData");
// Then fetch its value attribute
var sSerializedData = serializedElement.GetAttribute("value");
// Finally, do something with sSerializedData
}
7) Finally, add the javascript method to the page that is fired when the silverlight control is loaded:
<script type="text/javascript">
function GetDiagramPageContent() {
/// <summary>This method retrieves the diagram page object content</summary>
// Exceptions are handled by the caller
var oObject = document.getElementById('objDiagram');
if (oObject) {
return oObject.Content;
} else {
return null;
}
}
function RefreshDiagram() {
try {
var oContent = GetDiagramPageContent();
try {
// If we don't have content or a diagram page, bail
if ((oContent == null) || (oContent.DiagramPage == null)) {
return;
}
} catch (ex) {
return;
}
// Now ask the control to refresh the diagram
oContent.DiagramPage.RefreshDiagram();
} catch (ex) {
alert('Javascript Error (RefreshDiagram)\r' + ex.message);
}
}
</script>
Scenario 2
Scenario 2 is very similar to scenario 1, with the following changes:
1) Do not include the onLoad param in the silverlight object. Instead, call the RefreshDiagram javascript method from the client-side button click.
2) Do not show or hide the containing div in code-behind. Instead, use the style attributes to control the visibility:
<div id="divDiagram" runat="server" style="visibility: hidden; visibility: visible">
and in the javascript button click event:
var oDiv = document.getElementById("divDiagram");
oDiv.style.visibility = "";
oDiv.style.display = "";
3) Load the hidden text box in pageload instead of on the server-side button click.
This might help
Passing Objects between ASP.NET ans Silverlight Controls
best regards
You can use this thing called SilverlightSerializer by Mike Talbot

Categories