Используем HTML и WebBrowser control в качестве UI для обычных windows-приложений на C#

в 23:41, , рубрики: .net, C#, html, UI, ненормальное программирование

Как известно, контрол WebBrowser это просто обертка над ActiveX компонентом Internet Explorer. Следовательно он предоставляет доступ к полноценному layout-движку со всеми современными плюшками. А раз так, то попробуем (сам не знаю правда зачем) на его основе сделать пользовательский интерфейс для обычного windows-приложения.

Можно, конечно, было бы запустить в этом же процессе мини веб-сервер (на HttpListener например) и ловить запросы через него, но это слишком просто, скучно и неспортивно. Попробуем обойтись без сетевых компонентов, just for fun.

Итак, задача проста — необходимо перехватывать отправку HTML-форм и выводить новый HTML-документ, сгенерированный логикой приложения в зависимости от POST-параметров.

Прежде всего нам понадобится Windows Forms Application с одной единственной формой на которой будет один единственный контрол — WebBrowser занимающий все пространство.

Для примера рисуем пару простых страниц интерфейса (с минимальным содержимым для краткости, а так скрипты и стили добавлять по вкусу).

Первая страница:

<!DOCTYPE html>
<html>
<head><meta http-equiv="X-UA-Compatible" content="IE=11"></head>
<body>
    <form method="post">
        <input type="text" name="TEXT1" value="Some text" />
        <input type="submit" value="Open page 2" name="page2" />
    </form>
</body>
</html>

Вторая страница:

<!DOCTYPE html>
<html>
<head><meta http-equiv="X-UA-Compatible" content="IE=11"></head>
<body>
    <div>%TEXT1%</div>
    <form method="post">
        <input type="submit" value="Back to page 1" name="page1" />
    </form>
</body>
</html>

Примечание: X-UA-Compatible необходим для корректного отображения некоторых стилей, в данном примере они не используются, но проблема есть. Не буду вдаваться в детали, но без этого компонент рисует страницы в режиме совместимости с чем то очень старым, со всеми вытекающими последствиями.

WebBrowser предоставляет несколько событий, среди которых есть например Navigating, которое срабатывает перед тем как отправить форму или перейти по ссылке.
Практически то что нужно, но это событие не предоставляет никакой информации о post-параметрах. GET-параметры использовать не получится поскольку у наших HTML-форм атрибут action отсутствует, что приведет к тому что в качестве URL всегда будет about:blank и никакой информации о GET параметрах не будет.
Для получения более подробной информации о запросе надо подписаться на событие BeforeNavigate2 (подробнее тут) у внутреннего COM-объекта браузера, благо он доступен через свойство ActiveXInstance.
Сделать это проще всего через dynamic, чтобы не возиться с объявлением COM-интерфейсов:
Объявляем делегат:

private delegate void BeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel);

Затем подписываемся на событие (в конструкторе формы у WebBrowser свойство ActiveXInstance будет null, потому это лучше сделать после того как окно загрузится, т.е. в OnLoad например):

((dynamic)webBrowser.ActiveXInstance).BeforeNavigate2 += new BeforeNavigate2(OnBeforeNavigate2);

Итак, в PostData лежат post-параметры в виде строки состоящей из пар ключ=значение объединенных через &. Разделяем их и укладываем в словарь:

var parameters = (PostData == null ? string.Empty : Encoding.UTF8.GetString(PostData))
    .Split('&')
    .Select(x => x.Split('='))
    .Where(x => x.Length == 2)
    .ToDictionary(x => WebUtility.UrlDecode(x[0]), x => WebUtility.UrlDecode(x[1]));

Кроме того, в этом обработчике лучше отменить действие через параметр Cancel, чтобы лишний раз не попадать на about:blank.

Имея параметры генерируем текст новой страницы. Сюда теоретически прикручивается какой-н менеджер обработчиков, выбирающий необходимый обработчик в зависимости от параметров, которые по каким-н шаблонам будут собирать страницы из кусочков, но для краткости пока для примера просто по кнопке page1 откроем первую страницу, по кнопке page2 — вторую (имена кнопок в разметке указывать обязательно, иначе из post-параметров не определить какую именно кнопку нажали), а также заменим все строки в круглых скобках на значения параметров с такими именами:

        private static string Handler(IReadOnlyDictionary<string, string> parameters)
        {
            // do some useful stuff here 
            var newPage = "Not found";
            if (parameters.ContainsKey(nameof(page1)))
                newPage = page1;
            if (parameters.ContainsKey(nameof(page2)))
                newPage = page2;
            parameters.ToList().ForEach(x => newPage = newPage.Replace("{" + x.Key + "}", x.Value));
            return newPage;
        }

Полученную строку с HTML-текстом записываем в свойство DocumentText WebBrowser-а.
И тут же получим бесконечный цикл перезагрузки страницы, поскольку установка этого свойства спровоцирует новый вызов OnBeforeNavigate2.
Временно отписаться от этого события не получится, поскольку вызывается оно откуда то из цикла обработки сообщений уже после того как установка DocumentText возвращает управление.
Т.о. необходимо всегда игнорировать каждый второй вызов обработчика, поскольку первое срабатывание это отправка формы в результате действий пользователя, которое обрабатывать нужно, а второе — отображение результата которое обрабатывать не нужно. Для простоты будем в начале обработчика OnBeforeNavigate2 переключать bool переменную.

ignore = !ignore;
if (ignore)
    return;

И вот результат:

Используем HTML и WebBrowser control в качестве UI для обычных windows-приложений на C# - 1

Это все что необходимо для минимального приложения. Но работать таким образом будет не все — за рамками осталось например получение данных о файлах для input type=«file», а также работа с XMLHttpRequest для корректной работы скриптов со всякими там ajax.

Исходный код

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Windows.Forms;

namespace TestHtmlUI
{
    internal sealed class MainForm : Form
    {
        private const string page1 = @"<!DOCTYPE html>
<html>
<head><meta http-equiv=""X-UA-Compatible"" content=""IE=11""></head>
<body>
    <form method = ""post"" >
        <input type=""text"" name=""TEXT1"" value=""Some text"" />
        <input type=""submit"" value=""Open page 2"" name=""" + nameof(page2) + @""" />
    </form>
</body>
</html>";

        private const string page2 = @"<!DOCTYPE html>
<html>
<head><meta http-equiv=""X-UA-Compatible"" content=""IE=11""></head>
<body>
    <div>{TEXT1}</div>
    <form method=""post"">
        <input type=""submit"" value=""Back to page 1"" name=""" + nameof(page1) + @""" />
    </form>
</body>
</html>";
        
        private delegate void BeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel);
        
        private readonly WebBrowser webBrowser = new WebBrowser { Dock = DockStyle.Fill };
        private bool ignore = true;

        private MainForm()
        {
            StartPosition = FormStartPosition.CenterScreen;
            Controls.Add(webBrowser);
            Load += (sender, e) => ((dynamic)webBrowser.ActiveXInstance).BeforeNavigate2 += new BeforeNavigate2(OnBeforeNavigate2);
            
            webBrowser.DocumentText = Handler(new Dictionary<string, string> { { nameof(page1), string.Empty } });
        }
        
        private void OnBeforeNavigate2(object pDisp, string url, int Flags, string TargetFrameName, byte[] PostData, string Headers, ref bool Cancel)
        {
            ignore = !ignore;
            if (ignore)
                return;
            Cancel = true;
            
            var parameters = (PostData == null ? string.Empty : Encoding.UTF8.GetString(PostData))
                .Split('&')
                .Select(x => x.Split('='))
                .Where(x => x.Length == 2)
                .ToDictionary(x => WebUtility.UrlDecode(x[0]), x => WebUtility.UrlDecode(x[1]));
            webBrowser.DocumentText = Handler(parameters);
        }

        [STAThread]
        private static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new MainForm());
        }

        private static string Handler(IReadOnlyDictionary<string, string> parameters)
        {
            var newPage = "Not found";
            if (parameters.ContainsKey(nameof(page1)))
                newPage = page1;
            if (parameters.ContainsKey(nameof(page2)))
                newPage = page2;
            // do dome usefull stuff here 
            parameters.ToList().ForEach(x => newPage = newPage.Replace("{" + x.Key + "}", x.Value));
            return newPage;
        }
    }
}

Автор: Einherjar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js