├── icon.ico
├── icon_cables.ico
├── App.xaml.cs
├── DemoDialog.xaml.cs
├── .github
└── workflows
│ └── build.yml
├── webdemoexe.sln
├── App.xaml
├── WebDemoExe.csproj
├── MainWindow.xaml
├── readme.md
├── DemoDialog.xaml
└── MainWindow.xaml.cs
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pandrr/WebDemoExe/HEAD/icon.ico
--------------------------------------------------------------------------------
/icon_cables.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pandrr/WebDemoExe/HEAD/icon_cables.ico
--------------------------------------------------------------------------------
/App.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | using System.Runtime.InteropServices;
6 | using System.Windows;
7 |
8 | namespace WebDemoExe
9 | {
10 | ///
11 | /// Interaction logic for App.xaml
12 | ///
13 | public partial class App : Application
14 | {
15 | public bool newRuntimeEventHandled = false;
16 |
17 | public App()
18 | {
19 | InitializeComponent();
20 | this.Resources["AdditionalArgs"] = "--enable-features=ThirdPartyStoragePartitioning,PartitionedCookies";
21 |
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/DemoDialog.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | using System.Diagnostics;
6 | using System.Drawing.Printing;
7 | using System.Runtime.InteropServices;
8 | using System.Windows;
9 |
10 | namespace WebDemoExe
11 | {
12 | ///
13 | /// Interaction logic for App.xaml
14 | ///
15 | public partial class DemoDialog :Window
16 | {
17 |
18 | public DemoDialog()
19 | {
20 | InitializeComponent();
21 | }
22 |
23 | private void okButton_Click(object sender, RoutedEventArgs e)
24 | {
25 | DialogResult = true;
26 |
27 | }
28 |
29 | private void fullscreen_Toggle(object sender, RoutedEventArgs e)
30 | {
31 | //Trace.WriteLine("toggle !!!", String(Fullscreen));
32 |
33 |
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | pull_request:
6 | branches: [ main ]
7 | paths:
8 | - '**.cs'
9 | - '**.csproj'
10 | # Allows you to run this workflow manually from the Actions tab
11 | workflow_dispatch: null
12 |
13 | env:
14 | DOTNET_VERSION: '8.0.311'
15 |
16 | jobs:
17 | build:
18 |
19 | name: build-${{matrix.os}}
20 | runs-on: ${{ matrix.os }}
21 | strategy:
22 | matrix:
23 | os: [windows-latest]
24 |
25 | steps:
26 | - uses: actions/checkout@v3
27 | - name: Setup .NET Core
28 | uses: actions/setup-dotnet@v3
29 | with:
30 | dotnet-version: ${{ env.DOTNET_VERSION }}
31 |
32 | - name: Install dependencies
33 | run: dotnet restore
34 |
35 | - name: Build
36 | run: dotnet build --configuration Release --no-restore
37 |
38 | - name: Upload Build Artifacts
39 | uses: actions/upload-artifact@v4
40 | with:
41 | name: build-artifacts
42 | path: ./bin
--------------------------------------------------------------------------------
/webdemoexe.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33516.290
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebDemoExe", "WebDemoExe.csproj", "{EE405166-276B-486B-A7C6-D3E5BE2BBB6C}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {EE405166-276B-486B-A7C6-D3E5BE2BBB6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {EE405166-276B-486B-A7C6-D3E5BE2BBB6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {EE405166-276B-486B-A7C6-D3E5BE2BBB6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {EE405166-276B-486B-A7C6-D3E5BE2BBB6C}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {D67C87AC-0755-417B-837B-CF60986F29DF}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
15 |
18 |
19 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/WebDemoExe.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net48
5 | true
6 | Hybrid Application Team
7 | holon
8 | WebDemoExe
9 | true
10 | Debug;Release
11 | icon.ico
12 | true
13 | 5
14 | true
15 | true
16 | latest
17 | true
18 |
19 |
20 | x64
21 |
22 |
23 | x86
24 |
25 |
26 | AnyCPU
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
36 |
43 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## webDemoExe
4 |
5 | [](https://github.com/pandrr/WebDemoExe/actions/workflows/build.yml)
6 |
7 | wrap your web demo into a windows exe format, just like a native demo.
8 |
9 | [download](https://github.com/pandrr/WebDemoExe/releases)
10 |
11 | ### about
12 |
13 | - it does not use electron, size is ~0.5mb
14 | - it will use edge to display your demo. edge is a chromium based browser, which is basically chrome
15 | - shows a little start dialog (only fullscreen option right now, more in the future hopefully)
16 |
17 | ### how
18 |
19 | - download the zip file from the releases section
20 | - put your static html/js files into the demo subfolder
21 | - edit webdemoexe.xml and change the title
22 | - rename webdemoexe.exe to your demo name
23 | - add `` into the config to not show the dialog at all and start directly, don't do this if you want to play audio without having another user interaction!
24 | - add `yourdomain.localhost` to customize the virtual host domain (defaults to webdemoexe.localhost)
25 |
26 | - if the url contains "webdemoexe_exit" it will exit, e.g. use window.location.hash="webdemoexe_exit"
27 |
28 | ### technical
29 | - exe is not signed, still have to click "run anyway", like with most demos
30 | - webdemoexe uses [webview2](https://learn.microsoft.com/en-us/microsoft-edge/webview2/) and creates a virtual host from the demo subfolder to run your demo
31 | - escape to close is handled by webdemoexe
32 | - if you need to handle escape key manually, add `` in the config.
33 | - no gesture is needed to auto play audio, if you normally display a play button, make sure it only shows when audiocontext stats is not "running"...
34 |
35 | ### ideas
36 | - currently has no resolution selection, not sure how this is possible with wpf etc.
37 | - in the future the dialog could show link to website/online version and maybe a little teaser image...
38 | - there should be way to exit the app from js / in electron we always used `window.close()` not possible with webview2 afaik
39 |
40 | ### misc
41 |
42 | thanks to kb for helping with initial setup!
43 |
44 | any help is appreciated. i am not a windows developer, i hope everything here is not too wrong.
45 |
46 |
--------------------------------------------------------------------------------
/DemoDialog.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
20 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | using System;
6 | using System.Collections.Generic;
7 | using System.ComponentModel;
8 | using System.Diagnostics;
9 | using System.Linq;
10 | using System.Text;
11 | using System.Windows;
12 | using System.Windows.Input;
13 | using System.IO;
14 | using Microsoft.Web.WebView2.Core;
15 | using Microsoft.Web.WebView2.Wpf;
16 | using System.Runtime.Remoting.Messaging;
17 | using System.Xml;
18 |
19 | namespace WebDemoExe
20 | {
21 | ///
22 | /// Interaction logic for MainWindow.xaml
23 | ///
24 | public partial class MainWindow : Window
25 | {
26 | private string _appDomain = "webdemoexe.localhost";
27 |
28 | CoreWebView2Environment _webViewEnvironment;
29 | CoreWebView2Environment WebViewEnvironment
30 | {
31 | get
32 | {
33 | if (_webViewEnvironment == null && webView?.CoreWebView2 != null)
34 | {
35 | _webViewEnvironment = webView.CoreWebView2.Environment;
36 | }
37 |
38 | return _webViewEnvironment;
39 | }
40 | }
41 |
42 | List _webViewFrames = new List();
43 |
44 | IDictionary<(string, CoreWebView2PermissionKind, bool), bool> _cachedPermissions =
45 | new Dictionary<(string, CoreWebView2PermissionKind, bool), bool>();
46 |
47 | bool _propagateKeys = false;
48 |
49 | public MainWindow()
50 | {
51 | var dlg = new DemoDialog();
52 |
53 |
54 | var configFile = "webdemoexe.xml";
55 | var reader = new XmlTextReader(configFile);
56 | reader.WhitespaceHandling = WhitespaceHandling.None;
57 |
58 | var currentTag = "";
59 | var dialogTitle = "webDemoExe";
60 | var autostart = false;
61 |
62 | try
63 | {
64 |
65 | while (reader.Read())
66 | {
67 | switch (reader.NodeType)
68 | {
69 | case XmlNodeType.Element:
70 | currentTag = reader.Name;
71 |
72 | if (reader.IsEmptyElement)
73 | {
74 | if (currentTag.Equals("autostart")) autostart = true;
75 | if (currentTag.Equals("propagatekeys")) _propagateKeys = true;
76 | }
77 | break;
78 |
79 | case XmlNodeType.Text:
80 | if (currentTag.Equals("title")) dialogTitle = reader.Value;
81 | if (currentTag.Equals("domain")) _appDomain = reader.Value;
82 | if (currentTag.Equals("autostart")) autostart = XmlConvert.ToBoolean(reader.Value);
83 | if (currentTag.Equals("propagatekeys")) _propagateKeys = XmlConvert.ToBoolean(reader.Value);
84 | break;
85 | }
86 | }
87 |
88 | }
89 | catch (Exception e)
90 | {
91 | dialogTitle = "xml error";
92 |
93 | MessageBox.Show($"Could not parse {configFile}: {dialogTitle}: {e.Message}");
94 | }
95 |
96 |
97 |
98 | if (autostart==false)
99 | {
100 | dlg.Title = dialogTitle;
101 |
102 | dlg.ShowDialog();
103 |
104 |
105 | Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--autoplay-policy=no-user-gesture-required");
106 | Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", Path.GetTempPath());
107 |
108 | if (dlg.DialogResult == true)
109 | {
110 | }
111 | else
112 | {
113 | closeExe();
114 | return;
115 | }
116 | }
117 |
118 | DataContext = this;
119 | InitializeComponent();
120 | AttachControlEventHandlers(webView);
121 |
122 | if ((bool)dlg.Fullscreen.IsChecked)
123 | {
124 | this.WindowStyle = WindowStyle.None;
125 | this.Topmost = true;
126 | this.WindowState = WindowState.Maximized;
127 | }
128 |
129 | webView.Focus();
130 | }
131 |
132 | void AttachControlEventHandlers(WebView2 control)
133 | {
134 | //
135 | control.NavigationStarting += WebView_NavigationStarting;
136 | //
137 | //
138 | control.NavigationCompleted += WebView_NavigationCompleted;
139 | //
140 | control.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
141 | control.KeyDown += WebView_KeyDown;
142 | }
143 |
144 |
145 | private bool _isControlInVisualTree = true;
146 |
147 | void RemoveControlFromVisualTree(WebView2 control)
148 | {
149 | Layout.Children.Remove(control);
150 | _isControlInVisualTree = false;
151 | }
152 |
153 | void AttachControlToVisualTree(WebView2 control)
154 | {
155 | Layout.Children.Add(control);
156 | _isControlInVisualTree = true;
157 | }
158 |
159 | WebView2 GetReplacementControl(bool useNewEnvironment)
160 | {
161 | WebView2 replacementControl = new WebView2();
162 | ((System.ComponentModel.ISupportInitialize)(replacementControl)).BeginInit();
163 | // Setup properties and bindings.
164 | if (useNewEnvironment)
165 | {
166 | // Create a new CoreWebView2CreationProperties instance so the environment
167 | // is made anew.
168 | replacementControl.CreationProperties = new CoreWebView2CreationProperties();
169 | replacementControl.CreationProperties.BrowserExecutableFolder = webView.CreationProperties.BrowserExecutableFolder;
170 | replacementControl.CreationProperties.AdditionalBrowserArguments = webView.CreationProperties.AdditionalBrowserArguments;
171 | shouldAttachEnvironmentEventHandlers = true;
172 | }
173 | else
174 | {
175 | replacementControl.CreationProperties = webView.CreationProperties;
176 | }
177 |
178 | AttachControlEventHandlers(replacementControl);
179 | replacementControl.Source = webView.Source ?? new Uri("https://www.bing.com");
180 | ((System.ComponentModel.ISupportInitialize)(replacementControl)).EndInit();
181 |
182 | return replacementControl;
183 | }
184 |
185 | void WebView_ProcessFailed(object sender, CoreWebView2ProcessFailedEventArgs e)
186 | {
187 | void ReinitIfSelectedByUser(string caption, string message)
188 | {
189 | this.Dispatcher.InvokeAsync(() =>
190 | {
191 | var selection = MessageBox.Show(message, caption, MessageBoxButton.YesNo);
192 | if (selection == MessageBoxResult.Yes)
193 | {
194 | // The control cannot be re-initialized so we setup a new instance to replace it.
195 | // Note the previous instance of the control is disposed of and removed from the
196 | // visual tree before attaching the new one.
197 | if (_isControlInVisualTree)
198 | {
199 | RemoveControlFromVisualTree(webView);
200 | }
201 | webView.Dispose();
202 | webView = GetReplacementControl(false);
203 | AttachControlToVisualTree(webView);
204 | // Set background transparent
205 | webView.DefaultBackgroundColor = System.Drawing.Color.Transparent;
206 | }
207 | });
208 | }
209 |
210 | void ReloadIfSelectedByUser(string caption, string message)
211 | {
212 | this.Dispatcher.InvokeAsync(() =>
213 | {
214 | var selection = MessageBox.Show(message, caption, MessageBoxButton.YesNo);
215 | if (selection == MessageBoxResult.Yes)
216 | {
217 | webView.Reload();
218 | // Set background transparent
219 | webView.DefaultBackgroundColor = System.Drawing.Color.Transparent;
220 | }
221 | });
222 | }
223 |
224 | bool IsAppContentUri(Uri source)
225 | {
226 | return source.Host == _appDomain;
227 | }
228 |
229 | if (e.ProcessFailedKind == CoreWebView2ProcessFailedKind.FrameRenderProcessExited)
230 | {
231 | // A frame-only renderer has exited unexpectedly. Check if reload is needed.
232 | // In this sample we only reload if the app's content has been impacted.
233 | foreach (CoreWebView2FrameInfo frameInfo in e.FrameInfosForFailedProcess)
234 | {
235 | if (IsAppContentUri(new System.Uri(frameInfo.Source)))
236 | {
237 | System.Threading.SynchronizationContext.Current.Post((_) =>
238 | {
239 | ReloadIfSelectedByUser("App content frame unresponsive",
240 | "Browser render process for app frame exited unexpectedly. Reload page?");
241 | }, null);
242 | }
243 | }
244 |
245 | return;
246 | }
247 |
248 | // Show the process failure details. Apps can collect info for their logging purposes.
249 | this.Dispatcher.InvokeAsync(() =>
250 | {
251 | StringBuilder messageBuilder = new StringBuilder();
252 | messageBuilder.AppendLine($"Process kind: {e.ProcessFailedKind}");
253 | messageBuilder.AppendLine($"Reason: {e.Reason}");
254 | messageBuilder.AppendLine($"Exit code: {e.ExitCode}");
255 | messageBuilder.AppendLine($"Process description: {e.ProcessDescription}");
256 | MessageBox.Show(messageBuilder.ToString(), "Child process failed", MessageBoxButton.OK);
257 | });
258 |
259 | if (e.ProcessFailedKind == CoreWebView2ProcessFailedKind.BrowserProcessExited)
260 | {
261 | ReinitIfSelectedByUser("Browser process exited",
262 | "Browser process exited unexpectedly. Recreate webview?");
263 | }
264 | else if (e.ProcessFailedKind == CoreWebView2ProcessFailedKind.RenderProcessUnresponsive)
265 | {
266 | ReinitIfSelectedByUser("Web page unresponsive",
267 | "Browser render process has stopped responding. Recreate webview?");
268 | }
269 | else if (e.ProcessFailedKind == CoreWebView2ProcessFailedKind.RenderProcessExited)
270 | {
271 | ReloadIfSelectedByUser("Web page unresponsive",
272 | "Browser render process exited unexpectedly. Reload page?");
273 | }
274 | }
275 |
276 |
277 | void WebView_KeyDown(object sender, KeyEventArgs e)
278 | {
279 | if (e.IsRepeat || _propagateKeys) return;
280 |
281 | if (e.KeyboardDevice.IsKeyDown(Key.Escape))
282 | {
283 | closeExe();
284 | }
285 |
286 | /* bool ctrl = e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl);
287 | bool alt = e.KeyboardDevice.IsKeyDown(Key.LeftAlt) || e.KeyboardDevice.IsKeyDown(Key.RightAlt);
288 | bool shift = e.KeyboardDevice.IsKeyDown(Key.LeftShift) || e.KeyboardDevice.IsKeyDown(Key.RightShift);
289 | */ /*
290 | if (e.Key == Key.N && ctrl && !alt && !shift)
291 | {
292 | new MainWindow().Show();
293 | e.Handled = true;
294 | }
295 | else if (e.Key == Key.W && ctrl && !alt && !shift)
296 | {
297 | closeExe();
298 | e.Handled = true;
299 | }
300 | */
301 | }
302 |
303 | void WebView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
304 | {
305 | RequeryCommands();
306 | }
307 |
308 | void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
309 | {
310 | RequeryCommands();
311 | }
312 |
313 | private bool shouldAttachEnvironmentEventHandlers = true;
314 |
315 | private string GetSdkBuildVersion()
316 | {
317 | CoreWebView2EnvironmentOptions options = new CoreWebView2EnvironmentOptions();
318 |
319 | // The full version string A.B.C.D
320 | var targetVersionMajorAndRest = options.TargetCompatibleBrowserVersion;
321 | var versionList = targetVersionMajorAndRest.Split('.');
322 | if (versionList.Length != 4)
323 | {
324 | return "Invalid SDK build version";
325 | }
326 | // Keep C.D
327 | return versionList[2] + "." + versionList[3];
328 | }
329 |
330 | private string GetRuntimeVersion(CoreWebView2 webView2)
331 | {
332 | return webView2.Environment.BrowserVersionString;
333 | }
334 |
335 | private string GetAppPath()
336 | {
337 | return System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
338 | }
339 |
340 | private string GetRuntimePath(CoreWebView2 webView2)
341 | {
342 | int processId = (int)webView2.BrowserProcessId;
343 | try
344 | {
345 | Process process = System.Diagnostics.Process.GetProcessById(processId);
346 | var fileName = process.MainModule.FileName;
347 | return System.IO.Path.GetDirectoryName(fileName);
348 | }
349 | catch (ArgumentException e)
350 | {
351 | return e.Message;
352 | }
353 | catch (InvalidOperationException e)
354 | {
355 | return e.Message;
356 | }
357 | // Occurred when a 32-bit process wants to access the modules of a 64-bit process.
358 | catch (Win32Exception e)
359 | {
360 | return e.Message;
361 | }
362 | }
363 |
364 | private string GetStartPageUri(CoreWebView2 webView2)
365 | {
366 | string uri = $"https://{_appDomain}/index.html";
367 | if (webView2 == null)
368 | {
369 | return uri;
370 | }
371 | string sdkBuildVersion = GetSdkBuildVersion(),
372 | runtimeVersion = GetRuntimeVersion(webView2),
373 | appPath = GetAppPath(),
374 | runtimePath = GetRuntimePath(webView2);
375 | string newUri = $"{uri}?sdkBuild={sdkBuildVersion}&runtimeVersion={runtimeVersion}" +
376 | $"&appPath={appPath}&runtimePath={runtimePath}";
377 | return newUri;
378 | }
379 |
380 | void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
381 | {
382 | if (e.IsSuccess)
383 | {
384 | // Setup host resource mapping for local files
385 | webView.CoreWebView2.SetVirtualHostNameToFolderMapping(_appDomain, "demo", CoreWebView2HostResourceAccessKind.DenyCors);
386 | // Set StartPage Uri
387 | webView.Source = new Uri(GetStartPageUri(webView.CoreWebView2));
388 |
389 | //
390 | webView.CoreWebView2.ProcessFailed += WebView_ProcessFailed;
391 | //
392 | //
393 | webView.CoreWebView2.DocumentTitleChanged += WebView_DocumentTitleChanged;
394 | //
395 | //
396 | //webView.CoreWebView2.IsDocumentPlayingAudioChanged += WebView_IsDocumentPlayingAudioChanged;
397 | //
398 | //
399 | //webView.CoreWebView2.IsMutedChanged += WebView_IsMutedChanged;
400 | //
401 | //
402 | webView.CoreWebView2.PermissionRequested += WebView_PermissionRequested;
403 | //
404 | //webView.CoreWebView2.DOMContentLoaded += WebView_PermissionManager_DOMContentLoaded;
405 | //webView.CoreWebView2.WebMessageReceived += WebView_PermissionManager_WebMessageReceived;
406 |
407 | // The CoreWebView2Environment instance is reused when re-assigning CoreWebView2CreationProperties
408 | // to the replacement control. We don't need to re-attach the event handlers unless the environment
409 | // instance has changed.
410 | if (shouldAttachEnvironmentEventHandlers)
411 | {
412 | try
413 | {
414 | //
415 | WebViewEnvironment.BrowserProcessExited += Environment_BrowserProcessExited;
416 | //
417 | //
418 | WebViewEnvironment.NewBrowserVersionAvailable += Environment_NewBrowserVersionAvailable;
419 | //
420 | //
421 | //WebViewEnvironment.ProcessInfosChanged += WebView_ProcessInfosChanged;
422 | //
423 | }
424 | catch (NotImplementedException)
425 | {
426 |
427 | }
428 | shouldAttachEnvironmentEventHandlers = false;
429 | }
430 |
431 | webView.CoreWebView2.FrameCreated += WebView_HandleIFrames;
432 |
433 | return;
434 | }
435 |
436 | // ERROR_DELETE_PENDING(0x8007012f)
437 | if (e.InitializationException.HResult == -2147024593)
438 | {
439 | MessageBox.Show($"Failed to create webview, because the profile's name has been marked as deleted, please use a different profile's name.");
440 | closeExe();
441 | return;
442 | }
443 | MessageBox.Show($"WebView2 creation failed with exception = {e.InitializationException}");
444 | }
445 |
446 | //
447 | private bool shouldAttemptReinitOnBrowserExit = false;
448 |
449 | void Environment_BrowserProcessExited(object sender, CoreWebView2BrowserProcessExitedEventArgs e)
450 | {
451 | // Let ProcessFailed handler take care of process failure.
452 | if (e.BrowserProcessExitKind == CoreWebView2BrowserProcessExitKind.Failed)
453 | {
454 | return;
455 | }
456 | if (shouldAttemptReinitOnBrowserExit)
457 | {
458 | _webViewEnvironment = null;
459 | webView = GetReplacementControl(true);
460 | AttachControlToVisualTree(webView);
461 | shouldAttemptReinitOnBrowserExit = false;
462 | }
463 | }
464 | //
465 |
466 | void WebView_HandleIFrames(object sender, CoreWebView2FrameCreatedEventArgs args)
467 | {
468 | _webViewFrames.Add(args.Frame);
469 | args.Frame.Destroyed += WebViewFrames_DestoryedNestedIFrames;
470 | }
471 | void WebViewFrames_DestoryedNestedIFrames(object sender, object args)
472 | {
473 | var frameToRemove = _webViewFrames.SingleOrDefault(r => r.IsDestroyed() == 1);
474 | if (frameToRemove != null)
475 | _webViewFrames.Remove(frameToRemove);
476 | }
477 |
478 | //
479 | // A new version of the WebView2 Runtime is available, our handler gets called.
480 | // We close our WebView and set a handler to reinitialize it once the WebView2
481 | // Runtime collection of processes are gone, so we get the new version of the
482 | // WebView2 Runtime.
483 | void Environment_NewBrowserVersionAvailable(object sender, object e)
484 | {
485 | if (((App)Application.Current).newRuntimeEventHandled)
486 | {
487 | return;
488 | }
489 |
490 | ((App)Application.Current).newRuntimeEventHandled = true;
491 | System.Threading.SynchronizationContext.Current.Post((_) =>
492 | {
493 | UpdateIfSelectedByUser();
494 | }, null);
495 | }
496 | //
497 |
498 | void UpdateIfSelectedByUser()
499 | {
500 | // New browser version available, ask user to close everything and re-init.
501 | StringBuilder messageBuilder = new StringBuilder(256);
502 | messageBuilder.Append("We detected there is a new version of the WebView2 Runtime installed. ");
503 | messageBuilder.Append("Do you want to switch to it now? This will re-create the WebView.");
504 | var selection = MessageBox.Show(this, messageBuilder.ToString(), "New WebView2 Runtime detected", MessageBoxButton.YesNo);
505 | if (selection == MessageBoxResult.Yes)
506 | {
507 | // If this or any other application creates additional WebViews from the same
508 | // environment configuration, all those WebViews need to be closed before
509 | // the browser process will exit. This sample creates a single WebView per
510 | // MainWindow, we let each MainWindow prepare to recreate and close its WebView.
511 | CloseAppWebViewsForUpdate();
512 | }
513 | ((App)Application.Current).newRuntimeEventHandled = false;
514 | }
515 |
516 | private void CloseAppWebViewsForUpdate()
517 | {
518 | foreach (Window window in Application.Current.Windows)
519 | {
520 | if (window is MainWindow mainWindow)
521 | {
522 | mainWindow.CloseWebViewForUpdate();
523 | }
524 | }
525 | }
526 |
527 | private void CloseWebViewForUpdate()
528 | {
529 | // We dispose of the control so the internal WebView objects are released
530 | // and the associated browser process exits.
531 | shouldAttemptReinitOnBrowserExit = true;
532 | RemoveControlFromVisualTree(webView);
533 | webView.Dispose();
534 | }
535 |
536 |
537 |
538 |
539 | private void sourceChanged(object sender, CoreWebView2SourceChangedEventArgs e)
540 | {
541 |
542 | if (webView.Source.AbsoluteUri.Contains("webdemoexe_exit")) closeExe();
543 | }
544 |
545 |
546 | void RequeryCommands()
547 | {
548 | // Seems like there should be a way to bind CanExecute directly to a bool property
549 | // so that the binding can take care keeping CanExecute up-to-date when the property's
550 | // value changes, but apparently there isn't. Instead we listen for the WebView events
551 | // which signal that one of the underlying bool properties might have changed and
552 | // bluntly tell all commands to re-check their CanExecute status.
553 | //
554 | // Another way to trigger this re-check would be to create our own bool dependency
555 | // properties on this class, bind them to the underlying properties, and implement a
556 | // PropertyChangedCallback on them. That arguably more directly binds the status of
557 | // the commands to the WebView's state, but at the cost of having an extraneous
558 | // dependency property sitting around for each underlying property, which doesn't seem
559 | // worth it, especially given that the WebView API explicitly documents which events
560 | // signal the property value changes.
561 | CommandManager.InvalidateRequerySuggested();
562 | }
563 |
564 | void WebView_DocumentTitleChanged(object sender, object e)
565 | {
566 | //
567 | this.Title = webView.CoreWebView2.DocumentTitle;
568 | //
569 | }
570 |
571 |
572 | //
573 | void WebView_PermissionRequested(object sender, CoreWebView2PermissionRequestedEventArgs args)
574 | {
575 | // allow everything!
576 | args.State = CoreWebView2PermissionState.Allow;
577 | }
578 | //
579 |
580 |
581 | void closeExe()
582 | {
583 | webView.Source = new Uri("about:blank");
584 | webView.Dispose();
585 | Close();
586 | System.Environment.Exit(1);
587 | }
588 | }
589 | }
590 |
--------------------------------------------------------------------------------