using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.WinUI; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Ai; using Wino.Mail.Api.Contracts.Ai; using Wino.Mail.Api.Contracts.Common; using Wino.Mail.WinUI.Services; namespace Wino.Mail.WinUI.Controls; public sealed partial class AiActionsPanel : UserControl, IDisposable { public event EventHandler? CloseRequested; private readonly IWinoAccountProfileService _profileService = App.Current.Services.GetRequiredService(); private readonly IStoreManagementService _storeManagementService = App.Current.Services.GetRequiredService(); private readonly IMailDialogService _dialogService = App.Current.Services.GetRequiredService(); private readonly IAiActionOptionsService _optionsService = App.Current.Services.GetRequiredService(); private readonly IPreferencesService _preferencesService = App.Current.Services.GetRequiredService(); private bool _disposedValue; private bool _isRefreshing; private bool _isBusy; private AiActionType _lastConfigurableAction = AiActionType.Translate; private bool _hasCachedSummary; private CancellationTokenSource? _actionCancellationTokenSource; private IReadOnlyList _translateOptions = Array.Empty(); private IReadOnlyList _rewriteOptions = Array.Empty(); [GeneratedDependencyProperty(DefaultValue = AiActionType.None)] public partial AiActionType AvailableActions { get; set; } [GeneratedDependencyProperty] public partial IAiHtmlActionHost? HtmlHost { get; set; } public AiTranslateLanguageOption? SelectedTranslateLanguageOption { get; set; } public AiTranslateLanguageOption? SelectedSummarizeLanguageOption { get; set; } public AiRewriteModeOption? SelectedRewriteModeOption { get; set; } public AiActionsPanel() { InitializeComponent(); } public void CancelPendingOperation() { _actionCancellationTokenSource?.Cancel(); } public void Dispose() { if (_disposedValue) { return; } _disposedValue = true; CancelAndDisposeActionCancellationToken(); } partial void OnAvailableActionsChanged(AiActionType newValue) { UpdateActionAvailability(); ApplySelectedAction(SelectDefaultAction()); _ = RefreshCachedSummaryStateAsync(); } private void OnLoaded(object sender, RoutedEventArgs e) { LoadOptions(); UpdateActionAvailability(); ApplySelectedAction(SelectDefaultAction()); _ = RefreshAvailabilityAsync(); } private void OnUnloaded(object sender, RoutedEventArgs e) { CancelPendingOperation(); } private void LoadOptions() { // Save current selections before replacing ItemsSource (which clears SelectedItem). var previousTranslateCode = SelectedTranslateLanguageOption?.Code; var previousSummarizeCode = SelectedSummarizeLanguageOption?.Code; var previousRewriteMode = SelectedRewriteModeOption?.Mode; var preferredTranslateCode = string.IsNullOrWhiteSpace(previousTranslateCode) ? _preferencesService.AiDefaultTranslationLanguageCode : previousTranslateCode; var preferredSummarizeCode = string.IsNullOrWhiteSpace(previousSummarizeCode) ? _preferencesService.AiSummarizeLanguageCode : previousSummarizeCode; _translateOptions = _optionsService.GetTranslateLanguageOptions(); _rewriteOptions = _optionsService.GetRewriteModeOptions(); TranslateLanguageComboBox.ItemsSource = _translateOptions; SummarizeLanguageComboBox.ItemsSource = _translateOptions; RewriteModeComboBox.ItemsSource = _rewriteOptions; // Restore selection by matching on value, falling back to first item. SelectedTranslateLanguageOption = FindOption(_translateOptions, o => o.Code == preferredTranslateCode) ?? FindOption(_translateOptions, o => o.Code == "en-US") ?? (_translateOptions.Count > 0 ? _translateOptions[0] : null); SelectedSummarizeLanguageOption = FindOption(_translateOptions, o => o.Code == preferredSummarizeCode) ?? FindOption(_translateOptions, o => o.Code == "en-US") ?? (_translateOptions.Count > 0 ? _translateOptions[0] : null); SelectedRewriteModeOption = FindOption(_rewriteOptions, o => o.Mode == previousRewriteMode) ?? (_rewriteOptions.Count > 0 ? _rewriteOptions[0] : null); TranslateLanguageComboBox.SelectedItem = SelectedTranslateLanguageOption; SummarizeLanguageComboBox.SelectedItem = SelectedSummarizeLanguageOption; RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption; UpdateRewriteOptionState(); } private static T? FindOption(IReadOnlyList options, Func predicate) where T : class { foreach (var option in options) { if (predicate(option)) { return option; } } return null; } private void UpdateActionAvailability() { TranslateSegment.Visibility = HasAction(AiActionType.Translate) ? Visibility.Visible : Visibility.Collapsed; RewriteSegment.Visibility = HasAction(AiActionType.Rewrite) ? Visibility.Visible : Visibility.Collapsed; SummarizeSegment.Visibility = HasAction(AiActionType.Summarize) ? Visibility.Visible : Visibility.Collapsed; SummarizeCachedIndicator.Visibility = HasAction(AiActionType.Summarize) && _hasCachedSummary ? Visibility.Visible : Visibility.Collapsed; } private bool HasAction(AiActionType action) => (AvailableActions & action) == action; private AiActionType SelectDefaultAction() { if (HasAction(AiActionType.Translate)) { return AiActionType.Translate; } if (HasAction(AiActionType.Rewrite)) { return AiActionType.Rewrite; } if (HasAction(AiActionType.Summarize)) { return AiActionType.Summarize; } return AiActionType.None; } private void ApplySelectedAction(AiActionType action) { if (action is AiActionType.Translate or AiActionType.Rewrite) { _lastConfigurableAction = action; } ActionSelector.SelectedItem = action switch { AiActionType.Translate => TranslateSegment, AiActionType.Rewrite => RewriteSegment, AiActionType.Summarize => SummarizeSegment, _ => null }; TranslateOptionsPanel.Visibility = action == AiActionType.Translate ? Visibility.Visible : Visibility.Collapsed; RewriteOptionsPanel.Visibility = action == AiActionType.Rewrite ? Visibility.Visible : Visibility.Collapsed; SummarizeOptionsPanel.Visibility = action == AiActionType.Summarize ? Visibility.Visible : Visibility.Collapsed; } public async Task RefreshAvailabilityAsync() { if (_isRefreshing || _disposedValue) { return; } _isRefreshing = true; SetBusyUi(isBusy: false, showLoading: true); try { var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(true); if (account == null) { UpdateUsageSummary(string.Empty); UpdatePanelState(showSignedOut: true); return; } var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true); if (!hasAiPack) { UpdateUsageSummary(string.Empty); UpdatePanelState(showPurchase: true); return; } var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true); if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null) { UpdateUsageSummary( CreateUsageSummary(aiStatusResponse.Result), GetUsedCount(aiStatusResponse.Result), GetUsageLimit(aiStatusResponse.Result)); } else { UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed); } await RefreshCachedSummaryStateAsync().ConfigureAwait(true); ApplySelectedAction(SelectDefaultAction()); UpdatePanelState(showReady: true); } catch (OperationCanceledException) { } catch (Exception) { UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed); UpdatePanelState(showReady: true); } finally { _isRefreshing = false; SetBusyUi(_isBusy, showLoading: false); } } private static string CreateUsageSummary(AiStatusResultDto aiStatus) { if (aiStatus.Used is int used && aiStatus.MonthlyLimit is int limit && limit > 0) { return string.Format(Translator.AiActions_UsageSummary, used, limit); } return Translator.WinoAccount_Management_AiPackUsageLoadFailed; } private static int GetUsedCount(AiStatusResultDto aiStatus) => aiStatus.Used is int used ? used : 0; private static string CreateUsageSummary(QuotaInfoDto quotaInfo) { if (quotaInfo.Used is int used && quotaInfo.MonthlyLimit is int limit && limit > 0) { return string.Format(Translator.AiActions_UsageSummary, used, limit); } return Translator.WinoAccount_Management_AiPackUsageLoadFailed; } private static int GetUsedCount(QuotaInfoDto quotaInfo) => quotaInfo.Used is int used ? used : 0; private static int GetUsageLimit(QuotaInfoDto quotaInfo) => quotaInfo.MonthlyLimit is int limit && limit > 0 ? limit : 1000; private static int GetUsageLimit(AiStatusResultDto aiStatus) => aiStatus.MonthlyLimit is int limit && limit > 0 ? limit : 1000; private void UpdatePanelState(bool showLoading = false, bool showSignedOut = false, bool showPurchase = false, bool showReady = false) { LoadingPanel.Visibility = showLoading ? Visibility.Visible : Visibility.Collapsed; SignedOutPanel.Visibility = showSignedOut ? Visibility.Visible : Visibility.Collapsed; PurchasePanel.Visibility = showPurchase ? Visibility.Visible : Visibility.Collapsed; ReadyPanel.Visibility = showReady ? Visibility.Visible : Visibility.Collapsed; } private void UpdateUsageSummary(string usageText, int usedCount = 0) { UsageSummaryTextBlock.Text = usageText; UsageProgressBar.Maximum = 1000; UsageProgressBar.Value = Math.Min(usedCount, 1000); } private void UpdateUsageSummary(string usageText, int usedCount, int usageLimit) { var normalizedLimit = usageLimit > 0 ? usageLimit : 1000; UsageSummaryTextBlock.Text = usageText; UsageProgressBar.Maximum = normalizedLimit; UsageProgressBar.Value = Math.Min(usedCount, normalizedLimit); } private void SetBusyUi(bool isBusy, bool showLoading) { _isBusy = isBusy; BusyProgressBar.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed; ActionSelector.IsEnabled = !isBusy; TranslateLanguageComboBox.IsEnabled = !isBusy; SummarizeLanguageComboBox.IsEnabled = !isBusy; RewriteModeComboBox.IsEnabled = !isBusy; CustomRewriteTextBox.IsEnabled = !isBusy; RunTranslateButton.IsEnabled = !isBusy; RunRewriteButton.IsEnabled = !isBusy; RunSummarizeButton.IsEnabled = !isBusy; SignedOutPanel.IsHitTestVisible = !isBusy; PurchasePanel.IsHitTestVisible = !isBusy; if (showLoading) { UpdatePanelState(showLoading: true); } else if (ReadyPanel.Visibility == Visibility.Visible) { UpdatePanelState(showReady: true); } else if (SignedOutPanel.Visibility == Visibility.Visible) { UpdatePanelState(showSignedOut: true); } else if (PurchasePanel.Visibility == Visibility.Visible) { UpdatePanelState(showPurchase: true); } } private async void SignInButton_Click(object sender, RoutedEventArgs e) { if (_isBusy) { return; } var account = await _dialogService.ShowWinoAccountLoginDialogAsync(); if (account != null) { _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email), InfoBarMessageType.Success); } await RefreshAvailabilityAsync(); } private async void CreateAccountButton_Click(object sender, RoutedEventArgs e) { if (_isBusy) { return; } var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync(); if (account != null) { _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email), InfoBarMessageType.Success); } await RefreshAvailabilityAsync(); } private async void PurchaseButton_Click(object sender, RoutedEventArgs e) { if (_isBusy) { return; } SetBusyUi(isBusy: true, showLoading: false); try { var purchaseResult = await _storeManagementService.PurchaseAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true); if (purchaseResult == StorePurchaseResult.NotPurchased) { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_PurchaseStartFailed, InfoBarMessageType.Error); return; } var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(true); if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal)) { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_StoreSyncFailed, InfoBarMessageType.Error); return; } if (purchaseResult == StorePurchaseResult.AlreadyPurchased) { _dialogService.InfoBarMessage(Translator.Info_PurchaseExistsTitle, Translator.Info_PurchaseExistsMessage, InfoBarMessageType.Warning); } else { _dialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle, Translator.Info_PurchaseThankYouMessage, InfoBarMessageType.Success); } } catch (OperationCanceledException) { } catch (Exception) { _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_PurchaseStartFailed, InfoBarMessageType.Error); } finally { SetBusyUi(isBusy: false, showLoading: false); await RefreshAvailabilityAsync(); } } private void ActionSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (ReferenceEquals(ActionSelector.SelectedItem, TranslateSegment)) { ApplySelectedAction(AiActionType.Translate); return; } if (ReferenceEquals(ActionSelector.SelectedItem, RewriteSegment)) { ApplySelectedAction(AiActionType.Rewrite); return; } if (ReferenceEquals(ActionSelector.SelectedItem, SummarizeSegment)) { ApplySelectedAction(AiActionType.Summarize); _ = RefreshCachedSummaryStateAsync(); } } private void TranslateLanguageComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (TranslateLanguageComboBox.SelectedItem is AiTranslateLanguageOption option) { SelectedTranslateLanguageOption = option; _preferencesService.AiDefaultTranslationLanguageCode = option.Code; } } private void RewriteModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (RewriteModeComboBox.SelectedItem is AiRewriteModeOption option) { SelectedRewriteModeOption = option; UpdateRewriteOptionState(); } } private void SummarizeLanguageComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (SummarizeLanguageComboBox.SelectedItem is AiTranslateLanguageOption option) { SelectedSummarizeLanguageOption = option; _preferencesService.AiSummarizeLanguageCode = option.Code; } } private void UpdateRewriteOptionState() { var isCustom = SelectedRewriteModeOption?.IsCustom ?? false; RewriteDescriptionTextBlock.Text = SelectedRewriteModeOption?.Description ?? string.Empty; RewriteDescriptionTextBlock.Visibility = string.IsNullOrWhiteSpace(RewriteDescriptionTextBlock.Text) ? Visibility.Collapsed : Visibility.Visible; CustomRewriteTextBox.Visibility = isCustom ? Visibility.Visible : Visibility.Collapsed; } private async void RunTranslateButton_Click(object sender, RoutedEventArgs e) { await ExecuteAiActionAsync(AiActionType.Translate); } private async void RunRewriteButton_Click(object sender, RoutedEventArgs e) { await ExecuteAiActionAsync(AiActionType.Rewrite); } private async void RunSummarizeButton_Click(object sender, RoutedEventArgs e) { await ExecuteAiActionAsync(AiActionType.Summarize); } private void CloseButton_Click(object sender, RoutedEventArgs e) { CancelPendingOperation(); CloseRequested?.Invoke(this, EventArgs.Empty); } private async Task ExecuteAiActionAsync(AiActionType action) { if (_isBusy) { _dialogService.InfoBarMessage(Translator.Composer_AiBusyTitle, Translator.Composer_AiBusyMessage, InfoBarMessageType.Warning); return; } if (HtmlHost == null) { return; } CancelAndDisposeActionCancellationToken(); _actionCancellationTokenSource = new CancellationTokenSource(); var cancellationToken = _actionCancellationTokenSource.Token; SetBusyUi(isBusy: true, showLoading: false); try { if (action == AiActionType.Translate && SelectedTranslateLanguageOption == null) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error); return; } if (action == AiActionType.Rewrite && string.IsNullOrWhiteSpace(ResolveRewriteMode())) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error); return; } if (action == AiActionType.Summarize && SelectedSummarizeLanguageOption == null) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error); return; } if (action == AiActionType.Translate) { var cachedTranslation = await HtmlHost.TryGetCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); if (!string.IsNullOrWhiteSpace(cachedTranslation)) { await HtmlHost.ApplyHtmlResultAsync(cachedTranslation, cancellationToken).ConfigureAwait(true); return; } } if (action == AiActionType.Summarize) { var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); if (!string.IsNullOrWhiteSpace(cachedSummary)) { _hasCachedSummary = true; UpdateActionAvailability(); await ShowSummaryDialogAsync(cachedSummary).ConfigureAwait(true); return; } } var html = await HtmlHost.GetCurrentHtmlAsync(cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrWhiteSpace(html)) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_AiHtmlEmpty, InfoBarMessageType.Error); return; } var response = action switch { AiActionType.Translate => await _profileService.TranslateAsync(html, SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true), AiActionType.Rewrite => await _profileService.RewriteAsync(html, ResolveRewriteMode(), cancellationToken).ConfigureAwait(true), AiActionType.Summarize => await _profileService.SummarizeAsync(html, SelectedSummarizeLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true), _ => ApiEnvelope.Failure(ApiErrorCodes.ValidationFailed) }; cancellationToken.ThrowIfCancellationRequested(); if (!response.IsSuccess || response.Result == null || string.IsNullOrWhiteSpace(response.Result.Html)) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, WinoAccountAiErrorTranslator.Format(response.ErrorCode, null), InfoBarMessageType.Error); return; } if (response.Quota != null) { UpdateUsageSummary( CreateUsageSummary(response.Quota), GetUsedCount(response.Quota), GetUsageLimit(response.Quota)); } if (action == AiActionType.Translate) { await HtmlHost.SaveCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, response.Result.Html, cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true); return; } if (action == AiActionType.Summarize) { await HtmlHost.SaveCachedSummaryTextAsync(response.Result.Html, cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); _hasCachedSummary = true; UpdateActionAvailability(); var savedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true); cancellationToken.ThrowIfCancellationRequested(); await ShowSummaryDialogAsync(string.IsNullOrWhiteSpace(savedSummary) ? response.Result.Html : savedSummary).ConfigureAwait(true); return; } await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true); } catch (OperationCanceledException) { } catch (Exception ex) { _dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, WinoAccountAiErrorTranslator.Format(null, ex.Message), InfoBarMessageType.Error); } finally { SetBusyUi(isBusy: false, showLoading: false); if (_actionCancellationTokenSource != null) { _actionCancellationTokenSource.Dispose(); _actionCancellationTokenSource = null; } // Summarize no longer auto-switches back; the user explicitly selected the tab. } } private string ResolveRewriteMode() { if (SelectedRewriteModeOption == null) { return string.Empty; } if (!SelectedRewriteModeOption.IsCustom) { return SelectedRewriteModeOption.Mode; } return CustomRewriteTextBox.Text?.Trim() ?? string.Empty; } private async Task RefreshCachedSummaryStateAsync() { if (HtmlHost == null || !HasAction(AiActionType.Summarize)) { _hasCachedSummary = false; UpdateActionAvailability(); return; } try { var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(CancellationToken.None).ConfigureAwait(true); _hasCachedSummary = !string.IsNullOrWhiteSpace(cachedSummary); } catch (Exception) { _hasCachedSummary = false; } UpdateActionAvailability(); } private async Task ShowSummaryDialogAsync(string summary) { if (HtmlHost == null) { await _dialogService.ShowMessageAsync(summary, Translator.Composer_AiSummarize, WinoCustomMessageDialogIcon.Information); return; } var summaryTextBox = new TextBox { Text = summary, IsReadOnly = true, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, MinHeight = 240, MaxHeight = 420, BorderThickness = new Thickness(0), Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0)) }; var dialog = new ContentDialog { XamlRoot = XamlRoot, RequestedTheme = ActualTheme, Title = Translator.Composer_AiSummarize, PrimaryButtonText = Translator.Buttons_Save, SecondaryButtonText = Translator.Buttons_Close, DefaultButton = ContentDialogButton.Secondary, Content = new ScrollViewer { Content = summaryTextBox, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled } }; dialog.PrimaryButtonClick += async (sender, args) => { var deferral = args.GetDeferral(); try { var configuredSummarySavePath = _preferencesService.AiSummarySavePath; var path = !string.IsNullOrWhiteSpace(configuredSummarySavePath) && Directory.Exists(configuredSummarySavePath) ? Path.Combine(configuredSummarySavePath, HtmlHost.GetSuggestedSummaryFileName()) : await _dialogService.PickFilePathAsync(HtmlHost.GetSuggestedSummaryFileName()); if (string.IsNullOrWhiteSpace(path)) { args.Cancel = true; return; } await File.WriteAllTextAsync(path, summary); _dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.ClipboardTextCopied_Message, Path.GetFileName(path)), InfoBarMessageType.Success); } catch (Exception ex) { args.Cancel = true; _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error); } finally { deferral.Complete(); } }; await dialog.ShowAsync(); } private void CancelAndDisposeActionCancellationToken() { if (_actionCancellationTokenSource == null) { return; } _actionCancellationTokenSource.Cancel(); _actionCancellationTokenSource.Dispose(); _actionCancellationTokenSource = null; } }