Optimize ViewModel activation lifecycle (#10146)

* Optimize ViewModel activation lifecycle

* Fix

* Fix

* Fix

* Set period to 1s
This commit is contained in:
DHR60
2026-09-12 09:48:27 +00:00
committed by GitHub
parent 4f5a6be12d
commit cef40d38ee
8 changed files with 129 additions and 124 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
namespace ServiceLib.Base; namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject public class MyReactiveObject : ReactiveObject, IActivatableViewModel
{ {
protected static Config? _config; protected static Config? _config;
public ViewModelActivator Activator { get; } = new();
} }
+13 -19
View File
@@ -136,29 +136,23 @@ public static class Extension
.Replace("\n", replacement); .Replace("\n", replacement);
} }
public static async Task<TOutput> HandleSafe<TInput, TOutput>( public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, this Interaction<TInput, TOutput> interaction, TInput input,
TInput input,
TOutput defaultValue = default!, TOutput defaultValue = default!,
[CallerMemberName] string memberName = "", [CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "", [CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0) [CallerLineNumber] int lineNumber = 0)
{ {
try return Signal.Defer(() => interaction.Handle(input))
{ .Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
return await interaction.Handle(input); {
} Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
catch (UnhandledInteractionException<TInput, TOutput> ex) return Signal.Return(defaultValue);
{ })
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}"; .Catch<TOutput, Exception>(ex =>
Logging.SaveLog(title, ex); {
return defaultValue; Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
} return Signal.Return(defaultValue);
catch (Exception ex) });
{
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
Logging.SaveLog(title, ex);
return defaultValue;
}
} }
} }
@@ -213,7 +213,7 @@ public class ClashUIItem
public bool EnableMixinContent { get; set; } public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; } public int ProxiesSorting { get; set; }
public bool ProxiesAutoRefresh { get; set; } public bool ProxiesAutoRefresh { get; set; }
public int ProxiesAutoDelayTestInterval { get; set; } = 10; public int ProxiesRefreshInterval { get; set; } = 2;
public bool ConnectionsAutoRefresh { get; set; } public bool ConnectionsAutoRefresh { get; set; }
public int ConnectionsRefreshInterval { get; set; } = 2; public int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; } public List<ColumnItem> ConnectionsColumnItem { get; set; }
@@ -24,12 +24,19 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true); await ClashConnectionClose(true);
}); });
_ = Task.Factory.StartNew( this.WhenActivated(disposables =>
async () => await GetClashConnectionsTask(), {
CancellationToken.None, var cancelDisposable = new CancellationDisposable();
TaskCreationOptions.LongRunning, cancelDisposable.DisposeWith(disposables);
TaskScheduler.Default var token = cancelDisposable.Token;
);
Task.Factory.StartNew(
async () => await GetClashConnectionsTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
} }
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = []; public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
@@ -117,29 +124,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections(); await GetClashConnections();
} }
public async Task GetClashConnectionsTask() public async Task GetClashConnectionsTask(CancellationToken token = default)
{ {
var numOfExecuted = 1; try
while (true)
{ {
await Task.Delay(1000 * 5); var numOfExecuted = 1;
numOfExecuted++; while (true)
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{ {
continue; await Task.Delay(1000, token);
} numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0) if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{ {
continue; continue;
} }
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0) if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{ {
continue; continue;
}
await GetClashConnections();
} }
await GetClashConnections(); }
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashConnectionsTask", ex);
} }
} }
} }
@@ -55,12 +55,19 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
_ = Task.Factory.StartNew( this.WhenActivated(disposables =>
async () => await GetClashProxiesTask(), {
CancellationToken.None, var cancelDisposable = new CancellationDisposable();
TaskCreationOptions.LongRunning, cancelDisposable.DisposeWith(disposables);
TaskScheduler.Default var token = cancelDisposable.Token;
);
Task.Factory.StartNew(
async () => await GetClashProxiesTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
} }
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = []; public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
@@ -101,27 +108,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#region task #region task
public async Task GetClashProxiesTask() public async Task GetClashProxiesTask(CancellationToken token = default)
{ {
var numOfExecuted = 1; try
while (true)
{ {
await Task.Delay(1000 * 60); var numOfExecuted = 1;
numOfExecuted++; while (true)
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{ {
continue; await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ProxiesRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesRefreshInterval != 0)
{
continue;
}
await GetClashProxies();
} }
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0) }
{ catch (OperationCanceledException)
continue; {
} // Ignored
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0) }
{ catch (Exception ex)
continue; {
} Logging.SaveLog("GetClashProxiesTask", ex);
await GetClashProxies();
} }
} }
+24 -45
View File
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
public partial class MsgViewModel : MyReactiveObject public partial class MsgViewModel : MyReactiveObject
{ {
public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new(); public Interaction<string, RxVoid> ShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new(); private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable; private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked public int NumMaxMsg => 500;
public int NumMaxMsg { get; } = 500;
[Reactive] [Reactive]
public partial string MsgFilter { get; set; } public partial string MsgFilter { get; set; }
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
AppEvents.SendMsgViewRequested AppEvents.SendMsgViewRequested
.AsObservable() .AsObservable()
//.ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(EnqueueQueueMsg);
.Subscribe(content => _ = AppendQueueMsg(content));
this.WhenActivated(disposables =>
{
Signal.Every(TimeSpan.FromSeconds(1))
.Where(_ => AutoRefresh && AppManager.Instance.ShowInTaskbar)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => FlushQueueToView())
.DisposeWith(disposables);
});
} }
public void FlushQueueMsg() private void FlushQueueToView()
{ {
_ = AppendQueueMsg(string.Empty); if (!AutoRefresh || _queueMsg.IsEmpty)
}
private async Task AppendQueueMsg(string msg)
{
if (AutoRefresh == false)
{ {
return; return;
} }
EnqueueQueueMsg(msg);
if (!AppManager.Instance.ShowInTaskbar) if (!AppManager.Instance.ShowInTaskbar)
{ {
return; return;
} }
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0) var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var msg))
{ {
return; sb.Append(msg);
} }
try if (sb.Length > 0)
{ {
await Task.Delay(500).ConfigureAwait(false); ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
if (sb.Length > 0)
{
try
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
}
catch
{
_queueMsg.Enqueue(sb.ToString());
}
}
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
} }
} }
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
} }
} }
EnqueueWithLimit(msg); var formattedMsg = msg.EndsWith(Environment.NewLine)
if (!msg.EndsWith(Environment.NewLine)) ? msg
{ : msg + Environment.NewLine;
EnqueueWithLimit(Environment.NewLine);
} EnqueueWithLimit(formattedMsg);
} }
private void EnqueueWithLimit(string item) private void EnqueueWithLimit(string item)
+2 -5
View File
@@ -16,15 +16,12 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction => ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{ {
var msg = interaction.Input; var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg), ShowMsg(msg);
DispatcherPriority.ApplicationIdle);
interaction.SetOutput(RxVoid.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
}); });
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
+2 -7
View File
@@ -11,17 +11,12 @@ public partial class MsgView
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction => ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{ {
var msg = interaction.Input; var msg = interaction.Input;
Application.Current?.Dispatcher.Invoke(() => ShowMsg(msg);
{
ShowMsg(msg);
}, DispatcherPriority.ApplicationIdle);
interaction.SetOutput(RxVoid.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
}); });
btnCopy.Click += menuMsgViewCopyAll_Click; btnCopy.Click += menuMsgViewCopyAll_Click;