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

View File

@@ -1,6 +1,8 @@
namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject
public class MyReactiveObject : ReactiveObject, IActivatableViewModel
{
protected static Config? _config;
public ViewModelActivator Activator { get; } = new();
}

View File

@@ -136,29 +136,23 @@ public static class Extension
.Replace("\n", replacement);
}
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction,
TInput input,
public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, TInput input,
TOutput defaultValue = default!,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
try
{
return await interaction.Handle(input);
}
catch (UnhandledInteractionException<TInput, TOutput> ex)
{
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
Logging.SaveLog(title, ex);
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;
}
return Signal.Defer(() => interaction.Handle(input))
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
{
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
return Signal.Return(defaultValue);
})
.Catch<TOutput, Exception>(ex =>
{
Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
return Signal.Return(defaultValue);
});
}
}

View File

@@ -213,7 +213,7 @@ public class ClashUIItem
public bool EnableMixinContent { get; set; }
public int ProxiesSorting { 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 int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; }

View File

@@ -24,12 +24,19 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true);
});
_ = Task.Factory.StartNew(
async () => await GetClashConnectionsTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashConnectionsTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
}
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
@@ -117,29 +124,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections();
}
public async Task GetClashConnectionsTask()
public async Task GetClashConnectionsTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
while (true)
{
continue;
}
await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
}
await GetClashConnections();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashConnectionsTask", ex);
}
}
}

View File

@@ -55,12 +55,19 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
_ = Task.Factory.StartNew(
async () => await GetClashProxiesTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashProxiesTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
}
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
@@ -101,27 +108,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#region task
public async Task GetClashProxiesTask()
public async Task GetClashProxiesTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
while (true)
{
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)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
{
continue;
}
await GetClashProxies();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashProxiesTask", ex);
}
}

View File

@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
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 volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked
public int NumMaxMsg { get; } = 500;
public int NumMaxMsg => 500;
[Reactive]
public partial string MsgFilter { get; set; }
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
AppEvents.SendMsgViewRequested
.AsObservable()
//.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(content => _ = AppendQueueMsg(content));
.Subscribe(EnqueueQueueMsg);
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);
}
private async Task AppendQueueMsg(string msg)
{
if (AutoRefresh == false)
if (!AutoRefresh || _queueMsg.IsEmpty)
{
return;
}
EnqueueQueueMsg(msg);
if (!AppManager.Instance.ShowInTaskbar)
{
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);
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);
ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
}
}
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
}
}
EnqueueWithLimit(msg);
if (!msg.EndsWith(Environment.NewLine))
{
EnqueueWithLimit(Environment.NewLine);
}
var formattedMsg = msg.EndsWith(Environment.NewLine)
? msg
: msg + Environment.NewLine;
EnqueueWithLimit(formattedMsg);
}
private void EnqueueWithLimit(string item)

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.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg),
DispatcherPriority.ApplicationIdle);
ShowMsg(msg);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
});
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(

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.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Application.Current?.Dispatcher.Invoke(() =>
{
ShowMsg(msg);
}, DispatcherPriority.ApplicationIdle);
ShowMsg(msg);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
});
btnCopy.Click += menuMsgViewCopyAll_Click;