ฉันรู้ว่าฉันมาสายฉันเพิ่งเปลี่ยนวิธีจัดการเคอร์เซอร์นาฬิกาทราย (สถานะไม่ว่าง) ของแอปพลิเคชันของฉัน
โซลูชันที่เสนอนี้ซับซ้อนกว่าคำตอบแรกของฉัน แต่ฉันคิดว่าสมบูรณ์และดีกว่า
ฉันไม่ได้บอกว่าฉันมีวิธีแก้ปัญหาที่ง่ายหรือสมบูรณ์ แต่สำหรับฉันมันเป็นวิธีที่ดีที่สุดเพราะส่วนใหญ่จะแก้ไขปัญหาทั้งหมดที่ฉันมีในการจัดการสถานะที่วุ่นวายในแอปพลิเคชันของฉัน
ข้อดี:
- สามารถจัดการสถานะ "ไม่ว่าง" จากทั้งแบบจำลองและมุมมอง
- สามารถจัดการสถานะไม่ว่างได้หากไม่มี GUI แยกออกจาก GUI
- เธรดปลอดภัย (สามารถใช้จากเธรดใดก็ได้)
- สนับสนุน Busy override (แสดงลูกศรชั่วคราว) เมื่อมีหน้าต่าง (Dialog) ที่ควรมองเห็นได้กลางธุรกรรมที่ยาวมาก
- สามารถซ้อนการทำงานจำนวนมากที่มีพฤติกรรมไม่ว่างซึ่งแสดงนาฬิกาทรายคงที่ไม่ว่าจะเป็นส่วนหนึ่งของงานย่อยที่มีความยาวเล็กน้อย การมีนาฬิกาทรายซึ่งจะไม่เปลี่ยนบ่อยๆจากยุ่งเป็นเรื่องปกติไปจนถึงยุ่ง สถานะไม่ว่างคงที่ถ้าเป็นไปได้โดยใช้สแต็ก
- สนับสนุนการย่อยเหตุการณ์ด้วยตัวชี้ที่อ่อนแอเนื่องจากอินสแตนซ์อ็อบเจ็กต์ "ส่วนกลาง" เป็นแบบโกลบอล (จะไม่ถูกเก็บรวบรวมขยะ - จะถูกรูท)
รหัสแบ่งออกเป็นสองสามชั้น:
- ไม่มีคลาส GUI: "Global" ซึ่งจัดการสถานะไม่ว่างและควรเริ่มต้นเมื่อแอปพลิเคชันเริ่มต้นด้วยโปรแกรมมอบหมายงาน เนื่องจากเป็น Global (singleton) ฉันจึงเลือกที่จะมีเหตุการณ์ NotifyPropertyChanged ที่อ่อนแอเพื่อที่จะไม่ให้การอ้างอิงอย่างหนักกับใครก็ตามที่ต้องการรับการแจ้งเตือนการเปลี่ยนแปลงใด ๆ
- คลาส GUI: AppGlobal ที่เชื่อมต่อกับ Global และเปลี่ยนลักษณะเมาส์ตาม Gloab สถานะไม่ว่าง ควรเริ่มต้นด้วยผู้มอบหมายงานเมื่อเริ่มโปรแกรม
- คลาส GUI เพื่อช่วยให้ Dialog (Window) มีการทำงานของเมาส์ที่เหมาะสมเมื่อใช้ในการทำธุรกรรมที่ยาวนานซึ่งเมาส์ถูกวางทับเพื่อแสดงนาฬิกาทรายและต้องการให้ลูกศรปกติเมื่อมีการใช้งาน Dialog (Window)
- รหัสยังรวมถึงการอ้างอิงบางอย่าง
นี่คือการใช้งาน:
ในนั้น:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
Global.Init(Application.Current.Dispatcher);
AppGlobal.Init(Application.Current.Dispatcher);
การใช้งานที่ต้องการ:
using (Global.Instance.GetDisposableBusyState())
{
...
}
การใช้งานอื่น ๆ :
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);
}
เคอร์เซอร์หน้าต่างอัตโนมัติ:
public partial class DlgAddSignalResult : Window
{
readonly WindowWithAutoBusyState _autoBusyState = new WindowWithAutoBusyState();
public DlgAddSignalResult()
{
InitializeComponent();
Model = DataContext as DlgAddSignalResultModel;
_autoBusyState.Init(this);
}
รหัส:
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;
}
}
}
}