BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Creating an HTML UI for Desktop .NET Applications

Creating an HTML UI for Desktop .NET Applications

Bookmarks

Key takeaways

  • The UI looks of the desktop .NET applications can be significantly improved if created not with standard means, but using HTML.
  • Chromium-based browser components perform well rendering UI created with modern web instruments.
  • Support of JavaScript calls and DOM access are essential criteria to implement two-way communication and should be considered before going for HTML UI
  • The Chromium DevTools simplify debugging your HTML UI significantly, that’s why support of this set of tools can be crucial for the component.
  • It is possible to create the modern HTML UI not only for WPF applications, but also for Windows Forms.

 

Why Create HTML UI for Desktop Apps?

There are a great number of .NET applications built for desktop, meant for desktop and working great on desktop, but looking painfully archaic due to the UI, created with the good old Windows Forms or Windows Presentation Foundation (WPF).  This detail, while often insignificant for developers, can be perceived as deadly sin by the design-savvy user audience.

Unlike desktop, trends in web development evolve rapidly, constantly presenting new tools for ever better look of web apps. The mighty combo of HTML5 and CSS3 allows creating attractive and functional interfaces with loads of room for customization.

No wonder then that developers are looking for ways to employ the richness of the Web UI in desktop applications. The common approach to do this is to embed a browser component to render the HTML UI within the desktop app.

Choosing the Tool

When it comes to choosing a browser component for the .NET app, the choice is among a number of alternatives: from the default WebBrowser control to open source solutions and the components, licensed commercially.

Since we’re looking for options to display the UI created with the modern web tools, we’ll have to pass the default WebBrowser since it is lacking the essential functionality required to render complex pages that use HTML5 or CSS3.

Open source components, while being quite functional and generally pulling their weight, are not always appropriate for use in the commercial projects. This may be due to several reasons:

including corporate procurement policies that disallow the use of the open source software, and massive legal effort required to check and approve the licensing terms for hundreds of components.  Another important concern, often posed by the creators of the commercial products, is absence of dedicated support and no exact commitments for the fixes and updates of the open source solutions. In fact, when selecting an open source solution you are frequently left on your own with all its challenges. There are quite a number of teams who would gladly have someone qualified take care of the component-specific issues so that they are free to work on their product.

Finally, there’s a number of commercial browser components suitable for developers, willing to opt for the safety and support of the proprietary software. Picking one of the available alternatives is a matter of the one’s taste. However, there are some universal criteria to look at, when choosing a software provider, e.g.: availability and qualification of support, frequency of updates and compliance with the modern standards.

In this tutorial, we’ll use the proprietary Chromium-based web browser component – DotNetBrowser by TeamDev Ltd. It is well-documented, updated on a monthly basis and comes with the professional support packages for any team size.

The underlying Chromium engine allows DotNetBrowser to display modern UI, built with HTML, as well as more complicated things like WebGL, WebRTC and Flash. For the purpose of this tutorial we’ll use the browser component to tackle following tasks:

  • Load and display a web page as an integral part of a WPF Application
  • Implement two-way communication between C# and JS
  • Listen to DOM events
  • Debug the loaded content using Chrome Developer Tools

Moreover, among existing browser components for .NET only DotNetBrowser provides API that allows to work with the DOM directly without adding JavaScript injections to your code. This API significantly simplifies access and modification of the web page data from the .NET side, as you will be able to see further in this article.

Getting Started

To use DotNetBrowser in your project, you’ll need to download it from its page and while there obtain a free 30-days evaluation licence.  DotNetBrowser is also available at nuget.org as a regular NuGet package. You can install it directly from Visual Studio using its NuGet manager.

Also, there is a VSIX package in the Visual Studio Gallery. This package adds DotNetBrowser UI controls directly to the Visual Studio Toolbox, and then you can use them in the UI designer.

Using DotNetBrowser API

Let’s proceed to creating HTML5 UI for a WPF application with the DotNetBrowser.

Adding BrowserView to Window

DotNetBrowser supports XAML markup, so you can describe a BrowserView in XAML right after creating a sample WPF application and setting up the DotNetBrowser dependencies:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpf="clr-namespace:DotNetBrowser.WPF;assembly=DotNetBrowser"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <wpf:WPFBrowserView Name="MyBrowserView"/>
    </Grid>
</Window>

Now let’s load a URL and run the sample to be sure that everything is set properly:

And that’s it!

Chromium DevTools

DotNetBrowser supports Chromium Developer Tools, so it is possible to inspect HTML, view CSS styles, run various commands and debug JavaScript code right from the DotNetBrowser control. Enabling DevTools is described in the article “Remote Debugging” on DotNetBrowser support site. The obtained remote debugging URL can be then loaded into Google Chrome.

To try this feature, we’ll add a context menu with “Copy Debug URL To Clipboard” item to the component. Here is the modified XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpf="clr-namespace:DotNetBrowser.WPF;assembly=DotNetBrowser"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <wpf:WPFBrowserView Name="MyBrowserView">
            <wpf:WPFBrowserView.ContextMenu>
                <ContextMenu>
                  <MenuItem Header="Copy Debug URL To Clipboard" 
                            Click="MenuItem_Click"/>  
                </ContextMenu>
            </wpf:WPFBrowserView.ContextMenu>
        </wpf:WPFBrowserView>
    </Grid>
</Window>

​The modified source code looks like shown below:

using DotNetBrowser;
using DotNetBrowser.WPF;
using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            BrowserPreferences.SetChromiumSwitches(
                "--remote-debugging-port=9222");
            InitializeComponent();
            MyBrowserView.Browser.ContextMenuHandler = 
                new MyContextMenuHandler(MyBrowserView);
            MyBrowserView.Browser.LoadURL("google.com");
        }

        class MyContextMenuHandler : ContextMenuHandler
        {
            WPFBrowserView view;
            public MyContextMenuHandler(WPFBrowserView view)
            {
                this.view = view;
            }

            public void ShowContextMenu(ContextMenuParams parameters)
            {
                Application.Current.Dispatcher.Invoke(new Action(() =>
                {
                    System.Windows.Controls.ContextMenu popupMenu = 
                            view.ContextMenu;
                    popupMenu.PlacementTarget = view as UIElement;
                    popupMenu.IsOpen = true;
                }));
            }
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            Clipboard.SetText(MyBrowserView.Browser.GetRemoteDebuggingURL());
            System.Windows.MessageBox.Show(this, 
                "Remote debugging URL is copied to clipboard", 
                "Remote Debugging URL");
        }
    }
}

​Now let’s try to use remote debugging.

After selecting the menu item, a message box appears, saying:

After pasting and loading this URL in Google Chrome, we are able to inspect the loaded page.

Loading Web Page

When we are sure that everything is configured properly, the next step is to create a web page that will act as interface for our application. Here is the source code of the UI page we’ve created:

<html>
<head>
    <link href='https://fonts.googleapis.com/css?family=Open+Sans' 
          rel='stylesheet' type='text/css'>
    <style>
        body {
            background-color: DodgerBlue;
            color: white;
            font: 14px/1.72 'Open Sans', Arial, sans-serif;
        }

        form {
            height: 264px;
            width: 272px;
            margin: auto;
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            right: 0;
        }

        label {
            display: block;
            text-transform: capitalize;
            margin: 14px 0 6px;
        }

        input[type="text"] {
            height: 36px;
            width: 100%;
            padding: 5px 10px;
            color: #000;
            font-weight: normal;
            border: 1px solid #ccc;
            border-radius: 2px;
            -webkit-box-shadow: 0 0 0 1px #fff;
            box-shadow: 0 0 0 1px #fff;
            outline: none;
            font: 14px/1.72 'Open Sans', Arial, sans-serif;
        }

        input[name="lastname"] {
            margin-bottom: 22px;
        }

        input[type="text"]:focus {
            box-shadow: 0 0 0 1px #ffe5ac;
            border-color: #ffe5ac;
        }

        input[type="submit"] {
            height: 46px;
            background: 0 0;
            color: #fff;
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: .04em;
            text-align: center;
            white-space: nowrap;
            border: 2px solid #fff;
            border-radius: 4px;
            padding: 0 30px 0;
            vertical-align: middle;
            cursor: pointer;
            -webkit-transition: all .15s ease-in;
            transition: all .15s ease-in;
            font: 14px/1 'Open Sans', Arial, sans-serif;
        }

            input[type="submit"]:focus {
                background: rgba(255, 255, 255, .1);
                outline: 5px auto -webkit-focus-ring-color;
                outline-offset: -2px;
            }

            input[type="submit"]:active {
                background-image: none;
                outline: 0;
                -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
                box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
            }

            input[type="submit"]:hover {
                color: #fff;
                background: rgba(255, 255, 255, .1);
            }
    </style>
</head>
<body>
    <form action="#">
        <label for="firstname">First Name</label>
        <input type="text" id="firstname" name="firstname" value="John" />
        <label for="lastname">Last Name</label>
        <input type="text" id="lastname" name="lastname" value="Smith" />
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

After that, we’ll need to save this page as ui.html and include this page to our sample project. Then we’ll modify the LoadURL() call as shown below:

MyBrowserView.Browser.LoadURL(System.IO.Path.GetFullPath("ui.html"));

​After compiling and starting the sample application, the main window looks like this:

As you can see, this sample form already looks like an integral part of the application. But we still need to obtain its data when a Submit button is clicked.

DOM Events

DotNetBrowser provides DOM API that allows to access each and every part of the application. Moreover, we are able to register event listeners for particular DOM elements and be informed about the DOM events.

First of all, let’s add the FinishLoadingFrameEvent handler to the Browser:

MyBrowserView.Browser.FinishLoadingFrameEvent +=
        Browser_FinishLoadingFrameEvent;

​The event handler implementation:

void Browser_FinishLoadingFrameEvent(object sender, 
    DotNetBrowser.Events.FinishLoadingEventArgs e)
{
    if (e.IsMainFrame)
    {
        DOMDocument document = e.Browser.GetDocument();
        List<DOMNode> inputs = document.GetElementsByTagName("input");
        foreach (DOMNode node in inputs)
        {
            DOMElement element = node as DOMElement;
            if (element.GetAttribute("type").ToLower().Equals("submit"))
            {
                element.AddEventListener(DOMEventType.OnClick, 
                    OnSubmitClicked, false);
            }
        }
    }
}

    
This event handler should be installed right before LoadURL() is called.

The OnSubmitClicked is an event handler that reads the form data and displays a message with obtained values.

private void OnSubmitClicked(object sender, DOMEventArgs e)
{
    String firstname = "";
    String lastname = "";

    DOMDocument document = MyBrowserView.Browser.GetDocument();
    List<DOMNode> inputs = document.GetElementsByTagName("input");
    foreach (DOMNode node in inputs)
    {
        DOMElement element = node as DOMElement;
        if (element.GetAttribute("name").ToLower().Equals("firstname"))
        {
            firstname = element.GetAttribute("value");
        }
        if (element.GetAttribute("name").ToLower().Equals("lastname"))
        {
            lastname = element.GetAttribute("value");
        }
    }
    Application.Current.Dispatcher.Invoke(new Action(() =>
    {
        System.Windows.MessageBox.Show(this, "First name: " + 
            firstname + "\nLast name: " + lastname, "Form Data");
    }));
}

After running the application and clicking the Submit button, we can see the following message:

Improving Interaction

During further testing of this simple application, you will face the following problem: the first name and last name values are always equal to the “value” attribute of the field, even if the real value of the field has already been changed. This is the known behavior: we're running up against the difference between attributes and properties. When you set the value of the element from JavaScript or manually, you are actually setting the property 'value' but the attribute of the element in the page source is not changed. Fortunately, there is another way to get actual data from the form fields.

DotNetBrowser provides a specific API to access the ‘value’ property directly. Let’s modify the OnSubmitClicked event handler and obtain the actual values via the DOMInputElement class:

private void OnSubmitClicked(object sender, DOMEventArgs e)
{
    System.Threading.Tasks.Task.Run((Action)GetFormFieldValues);
}

private void GetFormFieldValues()
{
    String firstname = "";
    String lastname = "";

    var document = MyBrowserView.Browser.GetDocument();
    firstname = ((DOMInputElement)document.GetElementById("firstname")).Value;
    lastname = ((DOMInputElement)document.GetElementById("lastname")).Value;

    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
    { 
        System.Windows.MessageBox.Show(this, "First name: " + firstname + 
            "\nLast name: " + lastname, "Form Data");
    }));
}

  
As you can see, the values are obtained in a separate thread. Such approach allows to avoid deadlocks during call.

Let’s start the modified application and change the form data:

After the submit button is hit, the following message is displayed:

As you can see, the message contains up-to-date values.

Using VB.NET or WinForms

If you prefer VB.NET to C#, or require HTML UI for Windows Forms application, the general idea remains the same. If you use VB.NET, almost all the differences will be syntax-related.

If you use WinForms instead of WPF, the main difference would be in the process of adding the BrowserView control to your form. You can add it directly in the source code, but of course this is not an optimal way of doing it. It would be more natural to drag and drop the control to your form from the Visual Studio Toolbox. You can add DotNetBrowser controls to the Toolbox quite easily via the VSIX package available at the Visual Studio gallery. This package installs both WPF and WinForms controls to the Toolbox automatically.

When you have the BrowserView control added, next step would be to add a context menu, this time using the built in capabilities of WinForms. Then you can go back to follow this instruction starting with the Loading Web Page section.

Conclusion

As you can see, implementing the HTML-based UI in your .NET app doesn’t have to be painful. The approach described here can be used to effectively combine the advantages of the modern web technologies with your expertise in .NET.

About the Author

Anna Dolbina is a technical lead at TeamDev Ltd. A savvy .NET developer, Anna is in charge of development of DotNetBrowser – a company’s Chromium-based .NET browser component. Apart from .NET framework, she is interested in developing integration software in Java and C++, especially if the solution involves interaction with operating systems and native libraries and components.

 

Rate this Article

Adoption
Style

BT