Applies To: WinForms, VB.NET, CefSharp
Getting Chromium to show up inside a .NET WinForms application is relatively easy using CefSharp. CefSharp is an open source project which provides Embedded Chromium for .NET (WPF & WinForms). It’s a great way to get Chrome hosted inside of your .NET application.
You can get a simple browser up and running in 5 minutes – which I’ll show you first. There are some additional steps required to use local resources and to handle console messages and dialog prompts which I’ll also show you.
Embedding a Chromium Browser
There’s an easy example over on the CefSharp project page called the MinimalExample that will get you up and running quickly with a bunch of the basics. I’m going to walk you through some very quick steps to just get an embedded browser working in a WinForm.
Getting CefSharp
First, create a new WinForms project. I’m using VB.NET (but C# works great too, of course) and am targeting .NET 4.5. Once you’ve got a project, you’ll need to add the CefSharp binaries. This is done through NuGet and is really simple.
In Visual Studio choose PROJECT > Manage NuGet Packages… In the Package Manager window switch to Online and search for CefSharp. You’ll select CefSharp.WinForms (My version is 37.0.0 but the package should always have the latest stable release) and choose Install:
This will take just a few seconds while it also adds all of the dependent packages. In the end you’ll have 4 packages (CefSharp.WinForms, CefSharp.Common, cef.redist.x64 and cef.redist.x86).
Initial Build
CefSharp doesn’t support the default AnyCPU build configuration. You’ll need to choose BUILD > Configuration Manager… Then change the Active solution platform to either x64 or x86 (just choose new and it’ll walk you through it). Here’s what my Debug configuration looks like:
Go ahead and build the project to make sure there are no reference errors.
Adding the Browser
Open your form and slap a Panel control on there (I’ve named mine panBrowser and set it’s Dock property to Fill). This isn’t required, but it certainly makes it easier to move around when changing your form later.
Switch to the Form’s code and go to the New sub (Constructor in C#). Here’s the code:
Imports CefSharp.WinForms Imports CefSharp Public Class Form1 Private WithEvents browser As ChromiumWebBrowser Public Sub New() InitializeComponent() Dim settings As New CefSettings() CefSharp.Cef.Initialize(settings) browser = New ChromiumWebBrowser("http://thechriskent.com") With { .Dock = DockStyle.Fill } panBrowser.Controls.Add(browser) End Sub End Class
Be sure to make the appropriate references above (lines 1-2). In this code, we have a ChromiumWebBrowser object (line 6) that we create with a default address and a dock style of Fill (lines 14-15). We then add that control to the Panel we added above. The only other thing we need to do is to call the Cef.Initialize function (line 12). We’re just passing default settings for now (line 11). Run it and you should see something similar to this:
Congratulations, you’ve got Chrome in your Form!
Loading From Local Resources
In a previous article, Use Local Files in CefSharp, I showed you how to make the necessary objects to register a CefCustomScheme that would load resources from the local file system. I also posted a similar article, Use Embedded Resources in CefSharp, to demonstrate how to load files directly from your project’s manifest. Those articles will give you more detail about how to do this. If you’re following along in C# just head over to that article and copy the objects. Otherwise, here they are in VB.NET:
LocalSchemeHandler
Add a new Class to your project called LocalSchemeHandler.vb and copy/paste the following:
Imports CefSharp Imports System.IO Public Class LocalSchemeHandler Implements ISchemeHandler Public Function ProcessRequestAsync(request As IRequest, response As ISchemeHandlerResponse, requestCompletedCallback As OnRequestCompletedHandler) As Boolean Implements ISchemeHandler.ProcessRequestAsync Dim u As New Uri(request.Url) Dim filepath As String = u.Authority & u.AbsolutePath If File.Exists(filepath) Then Dim bytes As Byte() = File.ReadAllBytes(filepath) response.ResponseStream = New MemoryStream(bytes) Select Case Path.GetExtension(filepath) Case ".html" response.MimeType = "text/html" Case ".js" response.MimeType = "text/javascript" Case ".png" response.MimeType = "image/png" Case ".appcache" OrElse ".manifest" response.MimeType = "text/cache-manifest" Case Else response.MimeType = "application/octet-stream" End Select requestCompletedCallback() Return True End If Return False End Function End Class
LocalSchemeHandlerFactory
Add another new Class to your project called LocalSchemeHandlerFactory.vb and copy/paste the following:
Imports CefSharp Public Class LocalSchemeHandlerFactory Implements ISchemeHandlerFactory Public Function Create() As ISchemeHandler Implements ISchemeHandlerFactory.Create Return New LocalSchemeHandler End Function Public Shared ReadOnly Property SchemeName() As String Get Return "local" End Get End Property End Class
Registering the Scheme
To tell the browser to use the LocalSchemeHandler we just need to adjust our settings object before the Cef.Initalize function from our Form constructor above:
Dim settings As New CefSettings() settings.RegisterScheme(New CefCustomScheme() With { .SchemeName = LocalSchemeHandlerFactory.SchemeName, .SchemeHandlerFactory = New LocalSchemeHandlerFactory }) CefSharp.Cef.Initialize(settings)
Proving it Works
To make sure everything is working we’ll need to have an actual local resource to load. Open your application’s directory (mine is bin/x64/debug) and create a folder called web. Cut and paste the following into a text file and save it as index.html in that web folder:
<!DOCTYPE html> <html> <body> <h1>My First Heading</h1> <p>My first paragraph.</p> <img src="images/truck.png"/> <script type="text/javascript"> console.log("Hello from the console!"); //alert("Hello from a dialog!"); </script> </body> </html>
Create an images folder inside the web folder and save this image as truck.png in there:
None of these files need to be added to your project, they just need to be in the directory with your executable. Now if you switch the address from the browser initialization code above to local://web/index.html and run the project you should see something like this:
Mapping the Console
You might have noticed a <script> tag in our index.html above with a call to the console. CefSharp provides us with an easy event, ConsoleMessage, that allows us to grab those messages and do whatever we want with them. In our case we just want to display those messages in a TextBox.
I’ve added a Groupbox below the panBrowser Panel control from earlier and put a TextBox control called txtActivity inside it. I’ve set the txtActivity properties like so: Dock=Fill, Multiline=True and ScrollBars=Both. Now we just need to add the following code to our Form code right after the constructor:
Private Sub onBrowserConsoleMessage(sender As Object, e As CefSharp.ConsoleMessageEventArgs) Handles browser.ConsoleMessage If e.Line > 0 Then addActivity(String.Format("CONSOLE: {0} ({1}|{2})", e.Message, e.Source, e.Line)) Else addActivity(String.Format("CONSOLE: {0}", e.Message)) End If End Sub Private Sub addActivity(Message As String) If txtActivity.InvokeRequired Then txtActivity.Invoke(New Action(Of String)(AddressOf addActivity), Message) Else txtActivity.AppendText(Message & vbCrLf) End If End Sub
We’ve added a handler for the ConsoleMessage event (lines 25-31). The ConsoleMessageEventArgs object provides us with three useful properties: Message, Source and Line. We always receive the Message and depending on how the call to the console was made we may receive information about the Source. When the Line is greater than 0 we output the Message and the Source/Line information, otherwise it’s just the Message.
We’ve also added a helper sub, addActivity, to take care of handling displaying the output. This allows us to easily change our handling in the future but it also simplifies thread considerations. Our embedded browser is multithreaded and the ConsoleMessage event generally doesn’t fire on the main thread. This is the reason for the InvokeRequired/Invoke code in lines 34-36. The actual display happens in line 37 where we use the AppendText method to ensure our textbox scrolls appropriately. Run the project and should see something similar to this:
Handling Dialogs
What about dialog messages? If you uncomment the alert command from the index.html above and run the application you’ll see that in WinForms this gets automatically handled with a MsgBox:
In our application we’d like to redirect these to our Activity feed as well. In CefSharp, you override the default handling of dialogs (alerts, confirmations and prompts) by implementing an IJsDialogHandler object.
In our case we just want everything noted in the activity feed and ignored. To do this, add a class to your project called LogDialogHandler.vb and copy/paste the following code into it:
Imports CefSharp Public Class LogDialogHandler Implements IJsDialogHandler Private logReceiver As Action(Of String) Public Sub New(LogReceiverAction As Action(Of String)) logReceiver = LogReceiverAction End Sub Public Function OnJSAlert(browser As IWebBrowser, url As String, message As String) As Boolean Implements IJsDialogHandler.OnJSAlert logReceiver(String.Format("ALERT: {0}", message)) Return True End Function Public Function OnJSConfirm(browser As IWebBrowser, url As String, message As String, ByRef retval As Boolean) As Boolean Implements IJsDialogHandler.OnJSConfirm logReceiver(String.Format("CONFIRM: {0}", message)) retval = True Return True End Function Public Function OnJSPrompt(browser As IWebBrowser, url As String, message As String, defaultValue As String, ByRef retval As Boolean, ByRef result As String) As Boolean Implements IJsDialogHandler.OnJSPrompt logReceiver(String.Format("PROMPT: {0} ({1})", message, defaultValue)) result = defaultValue retval = True Return True End Function End Class
The IJsDialogHandler interface requires us to implement the OnJSAlert, OnJSConfirm and OnJSPrompt functions. We’ve also added a construtor that takes an Action(Of String) which we will use to handle our logging. We store this Action into our private logReceiver object (line 8) so that we can use it later. Then in our implementation of the functions we just call logReceiver with an informative message regarding what’s occurred. In each case we return True to indicate that the alert, etc. has been handled.
To use our new object we just add the following line to the end of our Form constructor:
browser.JsDialogHandler = New LogDialogHandler(New Action(Of String)(AddressOf addActivity))
This allows us to direct any message from our LogDialogHandler to our txtActivity box just like our console messages. Now when we run the application we should see something like this:
DevTools
Anyone that’s used Chrome for web development is very familiar with the F-12 DevTools and knows they can be invaluable in tracking down issues. Wouldn’t it be nice to have those when working within your .NET application? With CefSharp, it’s super easy. I’ve added a simple button underneath my Activity GroupBox called btnDevTools. Here’s the Click EventHandler:
Private Sub btnDevTools_Click(sender As Object, e As EventArgs) Handles btnDevTools.Click browser.ShowDevTools() End Sub
That’s it! Now when you push the button you get the full power of the Chrome DevTools all while running in your form:
Conclusion
Obviously, there is a lot you can do with this and there are many features we haven’t even begun to demonstrate, but this should get you started. For me the goal has been to get a Construct 2 game running in a .NET application. In my next post I’ll show you exactly that.