Tôi biết mình đến muộn, tôi vừa thay đổi cách quản lý con trỏ Đồng hồ cát (trạng thái bận) của ứng dụng.
Giải pháp được đề xuất này phức tạp hơn câu trả lời đầu tiên của tôi nhưng tôi nghĩ nó đầy đủ hơn và tốt hơn.
Tôi không nói rằng tôi có một giải pháp dễ dàng hay một giải pháp hoàn chỉnh. Nhưng đối với tôi nó là tốt nhất vì nó chủ yếu khắc phục tất cả những rắc rối mà tôi gặp phải khi quản lý trạng thái bận của ứng dụng của mình.
Ưu điểm:
- Có thể quản lý trạng thái "Bận" từ cả mô hình và chế độ xem.
- Có thể quản lý trạng thái bận nếu không có GUI. Được tách khỏi GUI.
- Két an toàn (có thể được sử dụng từ bất kỳ chủ đề nào)
- Hỗ trợ ghi đè Bận (hiển thị mũi tên tạm thời) khi có một Cửa sổ (Hộp thoại) hiển thị ở giữa một giao dịch rất dài.
- Có thể xếp chồng nhiều hoạt động với hành vi bận rộn hiển thị đồng hồ cát không đổi hoặc nếu nó là một phần của nhiều nhiệm vụ phụ nhỏ dài. Có một chiếc đồng hồ cát sẽ không thay đổi thường xuyên từ bận rộn sang bình thường sang bận rộn. Trạng thái bận liên tục nếu có thể, bằng cách sử dụng ngăn xếp.
- Hỗ trợ đăng ký sự kiện với con trỏ yếu vì cá thể đối tượng "Toàn cầu" là toàn cầu (sẽ không bao giờ được Thu gom rác - nó đã được root).
Mã được tách thành một số lớp:
- Không có lớp GUI: "Toàn cầu" quản lý trạng thái Bận và phải được khởi tạo khi bắt đầu ứng dụng với trình điều phối. Bởi vì nó là Toàn cầu (singleton), tôi đã chọn sự kiện NotifyPropertyChanged yếu để không giữ một lời giới thiệu cứng nhắc về bất kỳ ai muốn được thông báo về bất kỳ thay đổi nào.
- Một lớp GUI: AppGlobal kết nối với Toàn cầu và thay đổi giao diện Chuột phù hợp với trạng thái Gloab.Busy. Nó sẽ được khởi tạo với điều phối viên cũng khi bắt đầu chương trình.
- Một lớp GUI để giúp Hộp thoại (Cửa sổ) có hành vi chuột thích hợp khi được sử dụng trong một giao dịch dài trong đó chuột đã được ghi đè để hiển thị hình đồng hồ cát và muốn có mũi tên thông thường khi Hộp thoại (Cửa sổ) được sử dụng.
- Mã cũng bao gồm một số phụ thuộc.
Đây là cách sử dụng:
Trong đó:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
Global.Init(Application.Current.Dispatcher);
AppGlobal.Init(Application.Current.Dispatcher);
Cách sử dụng ưu tiên:
using (Global.Instance.GetDisposableBusyState())
{
...
}
Cách sử dụng khác:
public DlgAddAggregateCalc()
{
InitializeComponent();
Model = DataContext as DlgAddAggregateCalcViewModel;
this.Activated += OnActivated;
this.Deactivated += OnDeactivated;
}
private void OnDeactivated(object sender, EventArgs eventArgs)
{
Global.Instance.PullState();
}
private void OnActivated(object sender, EventArgs eventArgs)
{
Global.Instance.PushState(false);
}
Con trỏ cửa sổ tự động:
public partial class DlgAddSignalResult : Window
{
readonly WindowWithAutoBusyState _autoBusyState = new WindowWithAutoBusyState();
public DlgAddSignalResult()
{
InitializeComponent();
Model = DataContext as DlgAddSignalResultModel;
_autoBusyState.Init(this);
}
Mã:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace HQ.Util.General.WeakEvent
{
public sealed class SmartWeakEvent<T> where T : class
{
struct EventEntry
{
public readonly MethodInfo TargetMethod;
public readonly WeakReference TargetReference;
public EventEntry(MethodInfo targetMethod, WeakReference targetReference)
{
this.TargetMethod = targetMethod;
this.TargetReference = targetReference;
}
}
readonly List<EventEntry> eventEntries = new List<EventEntry>();
public int CountOfDelegateEntry
{
get
{
RemoveDeadEntries();
return eventEntries.Count;
}
}
static SmartWeakEvent()
{
if (!typeof(T).IsSubclassOf(typeof(Delegate)))
throw new ArgumentException("T must be a delegate type");
MethodInfo invoke = typeof(T).GetMethod("Invoke");
if (invoke == null || invoke.GetParameters().Length != 2)
throw new ArgumentException("T must be a delegate type taking 2 parameters");
ParameterInfo senderParameter = invoke.GetParameters()[0];
if (senderParameter.ParameterType != typeof(object))
throw new ArgumentException("The first delegate parameter must be of type 'object'");
ParameterInfo argsParameter = invoke.GetParameters()[1];
if (!(typeof(EventArgs).IsAssignableFrom(argsParameter.ParameterType)))
throw new ArgumentException("The second delegate parameter must be derived from type 'EventArgs'");
if (invoke.ReturnType != typeof(void))
throw new ArgumentException("The delegate return type must be void.");
}
public void Add(T eh)
{
if (eh != null)
{
Delegate d = (Delegate)(object)eh;
if (d.Method.DeclaringType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length != 0)
throw new ArgumentException("Cannot create weak event to anonymous method with closure.");
if (eventEntries.Count == eventEntries.Capacity)
RemoveDeadEntries();
WeakReference target = d.Target != null ? new WeakReference(d.Target) : null;
eventEntries.Add(new EventEntry(d.Method, target));
}
}
void RemoveDeadEntries()
{
eventEntries.RemoveAll(ee => ee.TargetReference != null && !ee.TargetReference.IsAlive);
}
public void Remove(T eh)
{
if (eh != null)
{
Delegate d = (Delegate)(object)eh;
for (int i = eventEntries.Count - 1; i >= 0; i--)
{
EventEntry entry = eventEntries[i];
if (entry.TargetReference != null)
{
object target = entry.TargetReference.Target;
if (target == null)
{
eventEntries.RemoveAt(i);
}
else if (target == d.Target && entry.TargetMethod == d.Method)
{
eventEntries.RemoveAt(i);
break;
}
}
else
{
if (d.Target == null && entry.TargetMethod == d.Method)
{
eventEntries.RemoveAt(i);
break;
}
}
}
}
}
public void Raise(object sender, EventArgs e)
{
int stepExceptionHelp = 0;
try
{
bool needsCleanup = false;
object[] parameters = {sender, e};
foreach (EventEntry ee in eventEntries.ToArray())
{
stepExceptionHelp = 1;
if (ee.TargetReference != null)
{
stepExceptionHelp = 2;
object target = ee.TargetReference.Target;
if (target != null)
{
stepExceptionHelp = 3;
ee.TargetMethod.Invoke(target, parameters);
}
else
{
needsCleanup = true;
}
}
else
{
stepExceptionHelp = 4;
ee.TargetMethod.Invoke(null, parameters);
}
}
if (needsCleanup)
{
stepExceptionHelp = 5;
RemoveDeadEntries();
}
stepExceptionHelp = 6;
}
catch (Exception ex)
{
string appName = Assembly.GetEntryAssembly().GetName().Name;
if (!EventLog.SourceExists(appName))
{
EventLog.CreateEventSource(appName, "Application");
EventLog.WriteEntry(appName,
String.Format("Exception happen in 'SmartWeakEvent.Raise()': {0}. Stack: {1}. Additional int: {2}.", ex.Message, ex.StackTrace, stepExceptionHelp), EventLogEntryType.Error);
}
throw;
}
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using System.Xml.Serialization;
using HQ.Util.General.Log;
using HQ.Util.General.WeakEvent;
using JetBrains.Annotations;
namespace HQ.Util.General.Notification
{
[Serializable]
public class NotifyPropertyChangedThreadSafeAsyncWeakBase : INotifyPropertyChanged
{
[XmlIgnore]
[field: NonSerialized]
public SmartWeakEvent<PropertyChangedEventHandler> SmartPropertyChanged = new SmartWeakEvent<PropertyChangedEventHandler>();
[XmlIgnore]
[field: NonSerialized]
private Dispatcher _dispatcher = null;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
SmartPropertyChanged.Add(value);
}
remove
{
SmartPropertyChanged.Remove(value);
}
}
[Browsable(false)]
[XmlIgnore]
public Dispatcher Dispatcher
{
get
{
if (_dispatcher == null)
{
_dispatcher = Application.Current?.Dispatcher;
if (_dispatcher == null)
{
if (Application.Current?.MainWindow != null)
{
_dispatcher = Application.Current.MainWindow.Dispatcher;
}
}
}
return _dispatcher;
}
set
{
if (_dispatcher == null && _dispatcher != value)
{
Debug.Print("Dispatcher has changed??? ");
}
_dispatcher = value;
}
}
[NotifyPropertyChangedInvocator]
protected void NotifyPropertyChanged([CallerMemberName] String propertyName = null)
{
try
{
if (Dispatcher == null || Dispatcher.CheckAccess())
{
SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
}
else
{
Dispatcher.BeginInvoke(
new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
}
}
catch (TaskCanceledException ex)
{
Log.Log.Instance.AddEntry(LogType.LogException, "An exception occured when trying to notify.", ex);
}
}
[NotifyPropertyChangedInvocator]
protected void NotifyPropertyChanged<T2>(Expression<Func<T2>> propAccess)
{
try
{
var asMember = propAccess.Body as MemberExpression;
if (asMember == null)
return;
string propertyName = asMember.Member.Name;
if (Dispatcher == null || Dispatcher.CheckAccess())
{
SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
}
else
{
Dispatcher.BeginInvoke(new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
}
}
catch (TaskCanceledException ex)
{
Log.Log.Instance.AddEntry(LogType.LogException, "An exception occured when trying to notify.", ex);
}
}
protected bool UpdateField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
NotifyPropertyChanged(propertyName);
return true;
}
return false;
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using HQ.Util.General.Notification;
namespace HQ.Util.General
{
public class Global : NotifyPropertyChangedThreadSafeAsyncWeakBase
{
public delegate void IsBusyChangeHandler(bool isBusy);
public event IsBusyChangeHandler IsBusyChange;
private readonly ConcurrentStack<bool> _stackBusy = new ConcurrentStack<bool>();
public static void Init(Dispatcher dispatcher)
{
Instance.Dispatcher = dispatcher;
}
public static Global Instance = new Global();
private Global()
{
_busyLifeTrackerStack = new LifeTrackerStack(() => PushState(), PullState);
}
public LifeTracker GetDisposableBusyState(bool isBusy = true)
{
return new LifeTracker(() => PushState(isBusy), PullState);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
private set
{
if (value == _isBusy) return;
_isBusy = value;
Dispatcher.BeginInvoke(new Action(() => IsBusyChange?.Invoke(_isBusy)), DispatcherPriority.ContextIdle);
NotifyPropertyChanged();
}
}
private readonly object _objLockBusyStateChange = new object();
public void PushState(bool isBusy = true)
{
lock (_objLockBusyStateChange)
{
_stackBusy.Push(isBusy);
IsBusy = isBusy;
}
}
public void PullState()
{
lock (_objLockBusyStateChange)
{
_stackBusy.TryPop(out bool isBusy);
if (_stackBusy.TryPeek(out isBusy))
{
IsBusy = isBusy;
}
else
{
IsBusy = false;
}
}
}
private readonly LifeTrackerStack _busyLifeTrackerStack = null;
[Obsolete("Use direct 'using(Gloabl.Instance.GetDisposableBusyState(isBusy))' which is simpler")]
public LifeTrackerStack BusyLifeTrackerStack
{
get { return _busyLifeTrackerStack; }
}
private int _currentVersionRequired = 0;
private readonly object _objLockRunOnce = new object();
private readonly Dictionary<int, GlobalRunOncePerQueueData> _actionsToRunOncePerQueue =
new Dictionary<int, GlobalRunOncePerQueueData>();
private readonly int _countOfRequestInQueue = 0;
public void RunOncePerQueueRollOnDispatcherThread(int key, Action action)
{
lock (_objLockRunOnce)
{
if (!_actionsToRunOncePerQueue.TryGetValue(key, out GlobalRunOncePerQueueData data))
{
data = new GlobalRunOncePerQueueData(action);
_actionsToRunOncePerQueue.Add(key, data);
}
_currentVersionRequired++;
data.VersionRequired = _currentVersionRequired;
}
if (_countOfRequestInQueue <= 1)
{
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(ExecuteActions));
}
}
private void ExecuteActions()
{
int versionExecute;
List<GlobalRunOncePerQueueData> datas = null;
lock (_objLockRunOnce)
{
versionExecute = _currentVersionRequired;
datas = _actionsToRunOncePerQueue.Values.ToList();
}
foreach (var data in datas)
{
data.Action();
}
lock (_objLockRunOnce)
{
List<int> keysToRemove = new List<int>();
foreach (var kvp in _actionsToRunOncePerQueue)
{
if (kvp.Value.VersionRequired <= versionExecute)
{
keysToRemove.Add(kvp.Key);
}
}
keysToRemove.ForEach(k => _actionsToRunOncePerQueue.Remove(k));
if (_actionsToRunOncePerQueue.Count == 0)
{
_currentVersionRequired = 0;
}
else
{
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using HQ.Util.General;
using HQ.Util.General.Notification;
using Microsoft.VisualBasic.Devices;
using System.Windows;
using Mouse = System.Windows.Input.Mouse;
using System.Threading;
namespace HQ.Wpf.Util
{
public class AppGlobal
{
public static void Init(Dispatcher dispatcher)
{
if (System.Windows.Input.Keyboard.IsKeyDown(Key.LeftShift) || System.Windows.Input.Keyboard.IsKeyDown(Key.RightShift))
{
var res = MessageBox.Show($"'{AppInfo.AppName}' has been started with shift pressed. Do you want to wait for the debugger (1 minute wait)?", "Confirmation", MessageBoxButton.YesNo,
MessageBoxImage.Exclamation, MessageBoxResult.No);
if (res == MessageBoxResult.Yes)
{
var start = DateTime.Now;
while (!Debugger.IsAttached)
{
if ((DateTime.Now - start).TotalSeconds > 60)
{
break;
}
Thread.Sleep(100);
}
}
}
if (dispatcher == null)
{
throw new ArgumentNullException();
}
Global.Init(dispatcher);
Instance.Init();
}
public static readonly AppGlobal Instance = new AppGlobal();
private AppGlobal()
{
}
private void Init()
{
Global.Instance.PropertyChanged += AppGlobalPropertyChanged;
}
void AppGlobalPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsBusy")
{
if (Global.Instance.IsBusy)
{
if (Global.Instance.Dispatcher.CheckAccess())
{
Mouse.OverrideCursor = Cursors.Wait;
}
else
{
Global.Instance.Dispatcher.BeginInvoke(new Action(() => Mouse.OverrideCursor = Cursors.Wait));
}
}
else
{
if (Global.Instance.Dispatcher.CheckAccess())
{
Mouse.OverrideCursor = Cursors.Arrow;
}
else
{
Global.Instance.Dispatcher.BeginInvoke(new Action(() => Mouse.OverrideCursor = Cursors.Arrow));
}
}
}
}
}
}
using HQ.Util.General;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace HQ.Wpf.Util
{
public class WindowWithAutoBusyState
{
Window _window;
bool _nextStateShoulBeVisible = true;
public WindowWithAutoBusyState()
{
}
public void Init(Window window)
{
_window = window;
_window.Cursor = Cursors.Wait;
_window.Loaded += (object sender, RoutedEventArgs e) => _window.Cursor = Cursors.Arrow;
_window.IsVisibleChanged += WindowIsVisibleChanged;
_window.Closed += WindowClosed;
}
private void WindowIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (_window.IsVisible)
{
if (_nextStateShoulBeVisible)
{
Global.Instance.PushState(false);
_nextStateShoulBeVisible = false;
}
}
else
{
if (!_nextStateShoulBeVisible)
{
Global.Instance.PullState();
_nextStateShoulBeVisible = true;
}
}
}
private void WindowClosed(object sender, EventArgs e)
{
if (!_nextStateShoulBeVisible)
{
Global.Instance.PullState();
_nextStateShoulBeVisible = true;
}
}
}
}