fix(sidebar): resolve MatchingService live + settings "סגור" label

Sidebar lookups threw ObjectDisposedException on the EspoCrmClient's
HttpClient after any settings save. TryInitializeCrm rebuilds the CRM
stack (new HttpClient + MatchingService) and disposes the old HttpClient,
but the long-lived sidebar ReadingPaneViewModel had captured the old
MatchingService and kept calling into the disposed client.

ReadingPaneViewModel now resolves the MatchingService live on each lookup
via a Func<IMatchingService?> provider (() => host.MatchingService), so
it always uses the current stack and degrades gracefully (shows "not
configured") when none exists. SidebarController no longer captures a
snapshot.

Also: rename the Settings dialog's bottom button from "ביטול" to "סגור".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
PointStar
2026-06-09 16:10:47 +03:00
parent 6a51a58224
commit d7a53da59f
5 changed files with 30 additions and 13 deletions
@@ -152,7 +152,7 @@
MinWidth="100" /> MinWidth="100" />
<Button Command="{Binding CancelCommand}" <Button Command="{Binding CancelCommand}"
IsCancel="True" IsCancel="True"
Content="ביטול" Content="סגור"
Padding="20,5" Padding="20,5"
MinWidth="100" /> MinWidth="100" />
</StackPanel> </StackPanel>
@@ -27,7 +27,12 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
public sealed partial class ReadingPaneViewModel : ObservableObject public sealed partial class ReadingPaneViewModel : ObservableObject
{ {
private readonly IMatchingService _matchingService; // Resolved live on each lookup rather than captured once: the host
// rebuilds the CRM stack (new HttpClient + MatchingService) whenever
// settings are saved and disposes the old HttpClient. A captured
// snapshot would then call into a disposed HttpClient and throw
// ObjectDisposedException on every sidebar lookup.
private readonly Func<IMatchingService?> _matchingServiceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
private CancellationTokenSource? _activeLookup; private CancellationTokenSource? _activeLookup;
// Stashes the ambiguous MatchResult while the user is drilling into // Stashes the ambiguous MatchResult while the user is drilling into
@@ -94,9 +99,9 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
/// </summary> /// </summary>
public event EventHandler<string>? OpenInBrowserRequested; public event EventHandler<string>? OpenInBrowserRequested;
public ReadingPaneViewModel(IMatchingService matchingService, ILogger logger) public ReadingPaneViewModel(Func<IMatchingService?> matchingServiceProvider, ILogger logger)
{ {
_matchingService = matchingService ?? throw new ArgumentNullException(nameof(matchingService)); _matchingServiceProvider = matchingServiceProvider ?? throw new ArgumentNullException(nameof(matchingServiceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -162,12 +167,20 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
_ambiguousReturnTarget = null; _ambiguousReturnTarget = null;
State = ReadingPaneState.Loading; State = ReadingPaneState.Loading;
var svc = _matchingServiceProvider();
if (svc == null)
{
ErrorMessage = "התוסף עדיין לא מוגדר. עדכן בהגדרות.";
State = ReadingPaneState.Error;
return;
}
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
_activeLookup = cts; _activeLookup = cts;
try try
{ {
var result = await _matchingService.LookupAsync(senderEmail, cts.Token).ConfigureAwait(true); var result = await svc.LookupAsync(senderEmail, cts.Token).ConfigureAwait(true);
if (cts.IsCancellationRequested) return; if (cts.IsCancellationRequested) return;
if (result == null) if (result == null)
@@ -296,6 +309,14 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
{ {
if (candidate == null || string.IsNullOrWhiteSpace(candidate.Id)) return; if (candidate == null || string.IsNullOrWhiteSpace(candidate.Id)) return;
var svc = _matchingServiceProvider();
if (svc == null)
{
ErrorMessage = "התוסף עדיין לא מוגדר. עדכן בהגדרות.";
State = ReadingPaneState.Error;
return;
}
CancelInFlight(); CancelInFlight();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
_activeLookup = cts; _activeLookup = cts;
@@ -310,7 +331,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
try try
{ {
var result = await _matchingService.LookupContactAsync(candidate.Id, rememberedEmail, cts.Token).ConfigureAwait(true); var result = await svc.LookupContactAsync(candidate.Id, rememberedEmail, cts.Token).ConfigureAwait(true);
if (cts.IsCancellationRequested) return; if (cts.IsCancellationRequested) return;
if (result == null || result.Contact == null) if (result == null || result.Contact == null)
+2 -2
View File
@@ -33,6 +33,6 @@ using System.Security;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.16.0")] [assembly: AssemblyVersion("1.2.17.0")]
[assembly: AssemblyFileVersion("1.2.16.0")] [assembly: AssemblyFileVersion("1.2.17.0")]
-1
View File
@@ -367,7 +367,6 @@ namespace OutlookAddin.Services
_sidebarController = new SidebarController( _sidebarController = new SidebarController(
taskPanes, taskPanes,
_outlookApp, _outlookApp,
MatchingService!,
this, this,
System.Windows.Threading.Dispatcher.CurrentDispatcher, System.Windows.Threading.Dispatcher.CurrentDispatcher,
Logger); Logger);
@@ -27,7 +27,6 @@ namespace OutlookAddin.Services
private readonly CustomTaskPaneCollection _taskPanes; private readonly CustomTaskPaneCollection _taskPanes;
private readonly Outlook.Application _app; private readonly Outlook.Application _app;
private readonly IMatchingService _matchingService;
private readonly AddInHost _host; private readonly AddInHost _host;
private readonly Dispatcher _dispatcher; private readonly Dispatcher _dispatcher;
private readonly ILogger _logger; private readonly ILogger _logger;
@@ -38,14 +37,12 @@ namespace OutlookAddin.Services
public SidebarController( public SidebarController(
CustomTaskPaneCollection taskPanes, CustomTaskPaneCollection taskPanes,
Outlook.Application app, Outlook.Application app,
IMatchingService matchingService,
AddInHost host, AddInHost host,
Dispatcher dispatcher, Dispatcher dispatcher,
ILogger logger) ILogger logger)
{ {
_taskPanes = taskPanes ?? throw new ArgumentNullException(nameof(taskPanes)); _taskPanes = taskPanes ?? throw new ArgumentNullException(nameof(taskPanes));
_app = app ?? throw new ArgumentNullException(nameof(app)); _app = app ?? throw new ArgumentNullException(nameof(app));
_matchingService = matchingService ?? throw new ArgumentNullException(nameof(matchingService));
_host = host ?? throw new ArgumentNullException(nameof(host)); _host = host ?? throw new ArgumentNullException(nameof(host));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -83,7 +80,7 @@ namespace OutlookAddin.Services
if (_bindings.ContainsKey(key)) return; if (_bindings.ContainsKey(key)) return;
var view = new ReadingPaneView(); var view = new ReadingPaneView();
var vm = new ReadingPaneViewModel(_matchingService, _logger) var vm = new ReadingPaneViewModel(() => _host.MatchingService, _logger)
{ {
EspoCrmBaseUrl = _host.Settings.EspoCrmUrl EspoCrmBaseUrl = _host.Settings.EspoCrmUrl
}; };