CefSharp.Wpf: Open csv, mailto and pdf with Process.Start(...) - wpf

I need to handle different content types from f:///. My application renders offline websites in a WPF application. Everything is working except for links to other content files (csv, mailto and pdf).
If I register a CefCustomScheme for "mailto", then I get the ProcessRequestAsync and can run the Process.Start(...). However another blank window also popup.
If I then add a second CefCustomScheme for "file", then nothing happens. None of the ISchemeHandler ProcessRequestAsync methods are invoked.
I must be able to handle all requests, excluding *.html, in a separate handler
Essentially I just want to replicate the behavior of the MS Web-browser Control. There all I did was point to the entry page (index.htm), and everything loaded. Then if a user clicks any link, the control handled the action and started the correct process (content handler, i.e. Excel for Csv).
The code:
// Startup
var settings = new CefSettings();
settings.LogFile = #"c:\temp\ceflog.txt";
settings.LogSeverity = LogSeverity.Verbose;
settings.IgnoreCertificateErrors = true;
CefCustomScheme mailtoScheme = new CefCustomScheme();
mailtoScheme.SchemeName = "mailto";
mailtoScheme.SchemeHandlerFactory = new SchemeHandlerFactory();
CefCustomScheme filesScheme = new CefCustomScheme();
mailtoScheme.SchemeName = "file";
mailtoScheme.SchemeHandlerFactory = new SchemeHandlerFactory();
settings.RegisterScheme(mailtoScheme);
settings.RegisterScheme(filesScheme);
if (!Cef.Initialize(settings))
throw new InvalidOperationException("Failed to initialize the browser factory");
-- SchemeHandlerFactory
public class SchemeHandlerFactory : ISchemeHandlerFactory {
public ISchemeHandler Create() {
return new CustomSchemeHandler();
}
}
-- Handler
public class CustomSchemeHandler : ISchemeHandler {
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public bool ProcessRequestAsync(IRequest request, ISchemeHandlerResponse response, OnRequestCompletedHandler requestCompletedCallback) {
_log.DebugFormat("Processing url: {0}", request.Dump());
var knownContentTypes = new[] {".csv", ".xsls", ".xlsx", ".pdf", ".txt"};
var ext=Path.GetExtension(request.Url);
if(knownContentTypes.Contains(ext)) {
_log.DebugFormat("Starting process for: {0}",request.Url);
Process.Start(request.Url);
return false;
}
return true;
}

The solution was to implement an IRequestHandler and use the OnBeforeResourceLoad event to check what content was requested. The ISchemeHandler is used for the "mailto" actions.
In my case I had to assign the request handler after the frame loaded. This allowed the web browser to render all content first.
Code sample GitHub example

Related

Element not found. How to test sequence of steps in multi-page WPF app with winappdriver?

I have a WPF application that features several pages. Navigation takes place after selecting some of the buttons. I am able to start the testing session successfully and to find elements in the main window. However, when clicking the "OK" button that should trigger navigation to another page, the app does not navigate and the test case fails to find UI elements in the next page. I have tried ImplicitWait and Thread.Sleep but the elements still cannot be found. It seems like the app does not navigate at all when the button is clicked.
Any ideas on how to tackle this issue? Below is what I have accomplished so far:
namespace TestDAAC
{
[TestClass]
public class UnitTests
{
protected const string WINAPPDRIVER_URL = "http://127.0.0.1:4723";
private const string DAAC_APP_ID = #"C:\Users\Admin\Desktop\VC PROJECTS\daac\DAAC\bin\x64\Release\DAAC5.exe";
protected static WindowsDriver<WindowsElement> daacSession;
[ClassInitialize]
public static void Setup(TestContext context)
{
if (daacSession == null)
{
var appiumOptions = new AppiumOptions();
appiumOptions.AddAdditionalCapability("app", DAAC_APP_ID);
appiumOptions.AddAdditionalCapability("deviceName", "WindowsPC");
daacSession = new WindowsDriver<WindowsElement>(new Uri(WINAPPDRIVER_URL), appiumOptions);
}
}
[TestMethod]
public void SequenceOfSteps()
{
daacSession.FindElementByName("OK").Click();
Thread.Sleep(5000);
// daacSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
daacSession.FindElementByName("New Imaging Session").Click();
}
}
}
I was able to find the reason why my test cases were not working. WinAppDriver is compatible only with Windows 10 Home or Pro. My target device runs on Windows Enterprise.

Accessing document elements when using Windows.Forms.WebBrowser

I'm new to automating webpage access, so forgive what is probably a remedial question. I'm using C#/Windows.Forms in a console app. I need to programmatically enter the value of an input on a webpage that I cannot modify and that is running javascript. I have successfully opened the page (triggering WebBrowser.DocumentCompleted). I set browser emulation mode to IE11 (in registry), so scripts run without errors. When DocumentCompleted() triggers, I am unable to access the document elements without first viewing the document content via MessageBox.Show(), which is clearly not acceptable for my unattended app.
What do I need to do so that my document elements are accessbile in an unattended session (so I can remove MessageBox.Show() from the code below)? Details below. Thank you.
The input HTML is:
<input class="input-class" on-keyup="handleKeyPress($key)" type="password">
My DocumentCompleted event handler is:
private static void LoginPageCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
WebBrowser wb = ((WebBrowser)sender);
var document = wb.Document;
// I'm trying to eliminate these 3 lines
var documentAsIHtmlDocument = (mshtml.IHTMLDocument)document.DomDocument;
var content = documentAsIHtmlDocument.documentElement.innerHTML;
MessageBox.Show(content);
String classname = null;
foreach (HtmlElement input in document.GetElementsByTagName("input"))
{
classname = input.GetAttribute("className");
if (classname == "input-class")
{
input.SetAttribute("value", password);
break;
}
}
}
The problem for me was that the page I'm accessing is being created by javascript. Even though documentComplete event was firing, the page was still not completely rendered. I have successfully processed the first page by waiting for the document elements to be available and if not available, doing Application.DoEvents(); in a loop until they are, so I know now that I'm on the right track.
This SO Question helped me: c# WebBrowser- How can I wait for javascript to finish running that runs when the document has finished loading?
Note that checking for DocumentComplete does not accurately indicate the availability of the document elements on a page generated by javascript. I needed to keep checking for the elements and running Application.DoEvents() until they became available (after the javascript generated them).
If the problem comes from the creation of a STAThread, necessary to instantiate the underlying Activex component of WebBrowser control, this is
a modified version of Hans Passant's code as shown in the SO Question you linked.
Tested in a Console project.
class Program
{
static void Main(string[] args)
{
NavigateURI(new Uri("[SomeUri]", UriKind.Absolute), "SomePassword");
Console.ReadLine();
}
private static string SomePassword = "SomePassword";
private static void NavigateURI(Uri url)
{
Thread thread = new Thread(() => {
WebBrowser browser = new WebBrowser();
browser.DocumentCompleted += browser_DocumentCompleted;
browser.Navigate(url);
Application.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
protected static void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
WebBrowser browser = ((WebBrowser)sender);
if (browser.Url == e.Url)
{
while (browser.ReadyState != WebBrowserReadyState.Complete)
{ Application.DoEvents(); }
HtmlDocument Doc = browser.Document;
if (Doc != null)
{
foreach (HtmlElement input in Doc.GetElementsByTagName("input"))
{
if (input.GetAttribute("type") == "password")
{
input.InnerText = SomePassword;
//Or
//input.SetAttribute("value", SomePassword);
break;
}
}
}
Application.ExitThread();
}
}
}

WPF + PRISM - Cannot 'LoadModule' in separate thread

I have a WPF application in PRISM architecture.
I have a 'Login View' that is shown in the 'Main Region' when the app loads.
When the user presses 'Login' - I connect to a WCF service, authenticate the user, and get a list of roles for that user from the service.
Then - according to the user's roles - I load different modules, using the 'Module Manager'.
Problem is - I want all the work after the 'Login' button is pressed to be done in a separate thread, because it might take time to connect to the service etc, and I don't want the UI to be frozen.
But - if I put the code to 'connect, authenticate, get roles, load modules' in a separate thread - I get an exception when I call '_moduleManager.LoadModule' that says:
The calling thread must be STA, because many UI components require this.
How can I solve this ?
I have tried different solutions.
I have tried to set the new thread's 'Apartment State = STA' and it didn't help.
I thought about saving the 'Dispatcher' object in the constructor of the View-Model, and then do 'dispatcher.Invoke' when I call 'LoadModule', but that is bad design (View-Model should not use Dispatcher, and also it is bad for testing).
Any ideas how I can solve this ??
Only the 'LoadModule' gives me grief, all the other stuff works fine.
.
[Update] - Added Code Sample :
[Export]
public class LoginViewModel : NotificationObject
{
[ImportingConstructor]
public LoginViewModel(IRegionManager regionManager, IModuleManager moduleManager)
{
this.LoginCommand = new DelegateCommand(LoginExecute, LoginCanExecute);
this._regionManager = regionManager;
this._moduleManager = moduleManager;
}
private void LoginExecute()
{
IsBusy = true; // Set this to 'true' so controls go disabled
LoginStatus = ""; // Clear the 'login status' string
Thread loginThread = new Thread(new ThreadStart(LoginWork));
loginThread.SetApartmentState(ApartmentState.STA);
loginThread.Start();
}
private void LoginWork()
{
ParamsToGetRoles param = new ParamsToGetRoles
{
Username = Username,
InputtedPassword = Password
};
try
{
// Connect to the secure service, and request the user's roles
_clientSecure = new AuthenticationServiceClient("WSHttpBinding_MyService");
_clientSecure.ClientCredentials.UserName.UserName = param.Username;
_clientSecure.ClientCredentials.UserName.Password = param.InputtedPassword;
_clientSecure.ChannelFactory.Faulted += new EventHandler(ChannelFactory_Faulted);
var local = _clientSecure.ChannelFactory.CreateChannel();
_clientSecure.GetRolesCompleted += new EventHandler<GetRolesCompletedEventArgs>(clientSecure_GetRolesCompleted);
_clientSecure.GetRolesAsync(param);
}
catch (Exception ex)
{
Console.WriteLine("Exception : " + ex.Message.ToString());
}
}
void clientSecure_GetRolesCompleted(object sender, GetRolesCompletedEventArgs e)
{
if (e.Error == null)
{
_clientSecure.Close();
LoginSuccess(e.Result.UserRoles);
}
else
{
LoginFailure("Unable to authenticate");
}
_clientSecure = null;
}
private void LoginSuccess(List<UserTypeEnum> rolesOfAuthenticatedUser)
{
LoginStatus = "Success";
if (rolesOfAuthenticatedUser.Contains(UserTypeEnum.Administrator))
{
// This is what throws the exception !
// This is called by the 'EndInvoke' of the 'GetRoles' operation,
// Which was called in the 'LoginWork' function which was run on a separate thread !
_moduleManager.LoadModule(WellKnownModuleNames.ModuleAdmin);
}
NavigateToMainMenu();
this.IsBusy = false;
}
}
You should attach the debugger and inspect the threads window with a breakpoint set at clientSecure_GetRolesCompleted. I'm pretty sure it is not being called from the loginThread: while LoginWork does run in the loginThread, it then adds an eventhandler to the completion event of an async operation. Async = runs in yet another thread.
So what probably happens:
LoginExecute executes in the UI thread
starts a seperate thread B to run LoginWork
calls GetRolesAsync so start a thread C (which is not STA) to get the roles
thread C eventually calls 'clientSecure_GetRolesCompleted', not thread B
So, you do not need a seperate thread for LoginWork since the actual work is already done as an async operation. To get around the loading issue, either try to make the 'get roles' thread STA, or better, use a dispatcher so LoginSuccess gets invoked on the UI thread.

Server Variables not making it from my ASP page to my Silverlight App

I have a basic .ASP page like this, which when hit redirects the user to my SL app after adding a value to the session variables
<!-- Default.htm -->
<html>
<%Session("valuekey")=somevalue%>
<Head>
<META http-equiv="Refresh" content="0; URL=/appdirectory/myapp.aspx?lsv=true"></HEAD>
<Body></Body>
</HTML>
When I get to my SL app hosted on myapp.aspx, the first think it does it check for the lsv QueryString. If it equals true, it calls a WCF service with code like
object x = HttpContext.Current.Session["valuekey"];
if(x == null)
{
ServiceError.Message = "No session variable found";
}
else
{
return x.ToString();
}
Does anyone know why the session variable that I just added on the ASP page before the redirect no longer exists when my SL app tried to fetch it?
This answer assumes there is a good reason why ASP classic enters into the equation in the first place. Perhaps its because Silverlight is being introduced into an existing ASP site. The problem is that most Silverlight Client-Server examples involve .NET WCF on the server.
The answer to your problem is don't use a WCF service to fetch your session data. Use a simple ASP page instead. It should be fairly straight-forward to use a simple XML structure to carry the session data you want to the Silverlight app. Use a DTO class that can be used to deserialise the XML into a simple class. Something like this:
(Caveat: air code)
<%
Dim dom: Set dom = CreateObject("MSXML2.DOMDocument.3.0")
dom.loadXML "<SessionData />"
AddElem dom.documentElement, "ValueKey", Session("valuekey")
AddElem dom.documentElement, "SomeOtherValue", Session("othervalue")
''# include other session values needed by client here.
Response.ContentType = "text/xml"
Response.CharSet = "utf-8"
dom.save Response
Sub AddElem(parent, name, value)
Dim elem: Set elem = parent.ownerDocument.createElement(name)
parent.appendChild elem
elem.text = value;
End Sub
%>
In Silverlight:
[DataContract]
public class SessionData
{
[DataMember(Order=1)]
public string ValueKey {get; set; }
[DataMember(Order=2)]
public string SomeOtherValue {get; set; }
public static void Fetch(Action<string> returnResult, Action<exception> fail)
{
WebClient client = new WebClient();
OpenReadCompletedEventHandler eh = null;
eh = (s, args) =>
{
try
{
var sr = new DataControlSerializer(typeof(SessionData));
returnResult((SessionData)sr.ReadObject(args.Result));
}
catch (Exception e)
{
fail(e);
}
finally
{
client.OpenReadAsyncCompleted -= eh;
}
};
client.OpenReadAsyncCompleted += eh;
client.OpenReadAsync(new Uri("../serviceFolder/sessionState.asp", UriKind.Relative));
}
}
Now in some UI or ViewModel you do
void SessionData_Available(SessionData sessionData)
{
_sessionData = sessionData;
// Other actions needed once session data has arrived.
}
void ReportProblem(Exception e)
{
// Some UI change to inform user of failed fetch
}
...
SessionData.Fetch(SessionData_Available, ReportProblem);
Your asp.net session variables are not available to Silverlight. They live in the server only.
Check this simple workaround. It might help you.

XAML to XPS memory leak

For a windows service project i have to make reports in xps format. I have xaml code that i turn into an xps document:
private void th_PrintErrorReport(OrderReportData reportData)
{
...
//Use the XAML reader to create a FlowDocument from the XAML string.
FlowDocument document = XamlReader.Load(new XmlTextReader(new StringReader(vRawXaml))) as FlowDocument;
//create xps file
using (XpsDocument xpsDoc = new XpsDocument(vFilePath, System.IO.FileAccess.Write, CompressionOption.Maximum))
{
// create a serialization manager
using (XpsSerializationManager rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false))
{
// retrieve document paginator
DocumentPaginator paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
// save as XPS
rsm.SaveAsXaml(paginator);
rsm.Commit();
}
}
}
This works but unfortunately creates a memory leak, each report created leaves the wpf controls (contentpresent, labels, etc) in memory. I checked this with a memory profiler. I checked topics like this one and this one which made me think that the wpf dispatcher/message pump is the problem. To make the message pump run i changed my code to:
public void StartHandling()
{
_ReportPrintingActive = true;
//xaml parsing has to run on a STA thread
_ReportPrintThread = new Thread(th_ErrorReportHandling);
_ReportPrintThread.SetApartmentState(ApartmentState.STA);
_ReportPrintThread.Name = "ErrorReportPrinter";
_ReportPrintThread.Start();
}
private void th_ErrorReportHandling()
{
Dispatcher.Run();
}
public void PrintErrorReport(OrderReportData reportData)
{
Action action = () =>
{
th_PrintErrorReport(reportData);
};
Dispatcher.FromThread(_ReportPrintThread).BeginInvoke(action);
}
But still no success. What am i missing ?
Using the reflection code from this post made the memory leak go away : https://stackoverflow.com/a/2410588/687462

Resources