Skip to content

Commit 9c92c94

Browse files
author
Thomas Felices
committed
Fix concurrent overlay issue [VPNWIN-2882]
1 parent 19495da commit 9c92c94

File tree

27 files changed

+512
-274
lines changed

27 files changed

+512
-274
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (c) 2025 Proton AG
3+
*
4+
* This file is part of ProtonVPN.
5+
*
6+
* ProtonVPN is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* ProtonVPN is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
using System;
21+
using Microsoft.UI.Xaml;
22+
using WinUIEx;
23+
24+
namespace ProtonVPN.Client.Common.UI.Controls.Custom;
25+
26+
public class BaseWindow : WindowEx
27+
{
28+
public bool IsLoaded { get; private set; }
29+
30+
public BaseWindow()
31+
{
32+
Activated += OnActivated;
33+
}
34+
35+
public event EventHandler? Loaded;
36+
37+
protected virtual void OnActivated(object sender, WindowActivatedEventArgs e)
38+
{
39+
if (!IsLoaded && Content is FrameworkElement rootElement)
40+
{
41+
rootElement.Loaded -= OnRootLoaded;
42+
rootElement.Loaded += OnRootLoaded;
43+
}
44+
}
45+
46+
protected virtual void OnLoaded()
47+
{ }
48+
49+
private void OnRootLoaded(object sender, RoutedEventArgs e)
50+
{
51+
IsLoaded = true;
52+
53+
OnLoaded();
54+
55+
Loaded?.Invoke(this, EventArgs.Empty);
56+
57+
if (sender is FrameworkElement rootElement)
58+
{
59+
rootElement.Loaded -= OnRootLoaded;
60+
}
61+
}
62+
}

src/Client/Common/ProtonVPN.Client.Common.UI/ProtonVPN.Client.Common.UI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@
896896
<PackageReference Include="Vanara.PInvoke.SHCore" />
897897
<PackageReference Include="Vanara.PInvoke.Shell32" />
898898
<PackageReference Include="Vanara.PInvoke.User32" />
899+
<PackageReference Include="WinUIEx" />
899900
</ItemGroup>
900901
<ItemGroup>
901902
<Page Update="Controls\Map\MapControl.xaml">

src/Client/ProtonVPN.Client.Core/Services/Activation/Bases/ActivatorBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public abstract class ActivatorBase<THost> : IActivator<THost>
2828

2929
public THost? Host { get; private set; }
3030

31+
protected string HostTypeName { get; private set; } = string.Empty;
32+
3133
protected ActivatorBase(
3234
ILogger logger)
3335
{
@@ -42,6 +44,8 @@ public void Initialize(THost host)
4244

4345
if (Host != null)
4446
{
47+
HostTypeName = Host.GetType().Name;
48+
4549
RegisterToHostEvents();
4650

4751
OnInitialized();
@@ -56,6 +60,8 @@ public void Reset()
5660

5761
UnregisterFromHostEvents();
5862

63+
HostTypeName = string.Empty;
64+
5965
Host = null;
6066
}
6167
}

src/Client/ProtonVPN.Client.Core/Services/Activation/Bases/DialogActivatorBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@
2020
using Microsoft.UI.Xaml;
2121
using ProtonVPN.Client.Common.Dispatching;
2222
using ProtonVPN.Client.Common.Messages;
23+
using ProtonVPN.Client.Common.UI.Controls.Custom;
2324
using ProtonVPN.Client.Contracts.Messages;
2425
using ProtonVPN.Client.Core.Extensions;
2526
using ProtonVPN.Client.Core.Services.Selection;
2627
using ProtonVPN.Client.EventMessaging.Contracts;
2728
using ProtonVPN.Client.Localization.Contracts;
2829
using ProtonVPN.Client.Settings.Contracts;
2930
using ProtonVPN.Logging.Contracts;
30-
using WinUIEx;
3131

3232
namespace ProtonVPN.Client.Core.Services.Activation.Bases;
3333

3434
public abstract class DialogActivatorBase<TWindow> : WindowActivatorBase<TWindow>,
3535
IEventMessageReceiver<MainWindowVisibilityChangedMessage>,
3636
IEventMessageReceiver<ApplicationStoppedMessage>
37-
where TWindow : WindowEx
37+
where TWindow : BaseWindow
3838
{
3939
protected readonly IMainWindowActivator MainWindowActivator;
4040

src/Client/ProtonVPN.Client.Core/Services/Activation/Bases/OverlayActivatorBase.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@
3333
using ProtonVPN.Client.Core.Extensions;
3434
using ProtonVPN.Client.Core.Services.Mapping;
3535
using ProtonVPN.Client.Core.Services.Selection;
36-
using WinUIEx;
36+
using System.Threading;
3737

3838
namespace ProtonVPN.Client.Core.Services.Activation.Bases;
3939

4040
public abstract class OverlayActivatorBase<TWindow> : WindowHostActivatorBase<TWindow>, IOverlayActivator
41-
where TWindow : WindowEx
41+
where TWindow : BaseWindow
4242
{
4343
protected readonly IOverlayViewMapper OverlayViewMapper;
4444

45+
private SemaphoreSlim _overlaySemaphore = new(0, 1);
46+
4547
private ContentDialog? _currentOverlay;
4648

4749
public bool HasActiveOverlay => _currentOverlay != null;
@@ -110,6 +112,14 @@ protected Task<ContentDialogResult> ShowOverlayAsync<TViewModel>(object? paramet
110112
return ShowOverlayAsync(overlayType, parameter);
111113
}
112114

115+
protected override void OnWindowLoaded()
116+
{
117+
base.OnWindowLoaded();
118+
119+
// Host ready, release the semaphore to allow showing overlays
120+
_overlaySemaphore.Release();
121+
}
122+
113123
protected override void RegisterToHostEvents()
114124
{
115125
base.RegisterToHostEvents();
@@ -171,7 +181,12 @@ private async Task<ContentDialogResult> ShowOverlayAsync(Type overlayType, objec
171181
}
172182

173183
private async Task<ContentDialogResult> ShowOverlayAsync(ContentDialog overlay, object? parameter = null)
174-
{
184+
{
185+
// Close current overlayViewModel if any, then register the new one. Cannot have two overlays at the same time
186+
CloseCurrentOverlay();
187+
188+
await _overlaySemaphore.WaitAsync();
189+
175190
try
176191
{
177192
RegisterOverlay(overlay);
@@ -189,22 +204,22 @@ private async Task<ContentDialogResult> ShowOverlayAsync(ContentDialog overlay,
189204
catch (Exception e)
190205
{
191206
Logger.Error<AppLog>($"Error when trying to show message '{overlay}'", e);
192-
throw;
207+
208+
return ContentDialogResult.None;
193209
}
194210
finally
195211
{
196212
UnregisterCurrentOverlay();
213+
214+
_overlaySemaphore.Release();
197215
}
198216
}
199217

200218
private void RegisterOverlay(ContentDialog overlay)
201219
{
202-
// Close current overlayViewModel if any, then register the new one. Cannot have two overlays at the same time
203-
CloseCurrentOverlay();
204-
205220
if (Host == null)
206221
{
207-
throw new InvalidOperationException("Window has not been initialized.");
222+
throw new InvalidOperationException("Host window has not been initialized.");
208223
}
209224

210225
overlay.XamlRoot = Host.GetXamlRoot();

0 commit comments

Comments
 (0)