menampilkan Jam Pasir saat aplikasi sedang sibuk

90

Untuk tampilan yang dibuat menggunakan WPF, saya ingin mengubah kursor mouse menjadi jam pasir saat aplikasi sedang sibuk dan tidak responsif.

Salah satu solusinya adalah menambahkan

 this.Cursor = Cursors.Wait;

ke semua tempat yang dapat menyebabkan UI menjadi tidak responsif. Tapi jelas ini bukan solusi terbaik. Saya bertanya-tanya apa cara terbaik untuk mencapai ini?

Apakah mungkin untuk mencapai ini dengan menggunakan Styles atau Resources?

Terima kasih,

sean717
sumber

Jawaban:

224

Kami melakukan kelas sekali pakai yang mengubah kursor untuk kami ketika aplikasi akan memakan waktu lama, terlihat seperti ini:

public class WaitCursor : IDisposable
{
    private Cursor _previousCursor;

    public WaitCursor()
    {
        _previousCursor = Mouse.OverrideCursor;

        Mouse.OverrideCursor = Cursors.Wait;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Mouse.OverrideCursor = _previousCursor;
    }

    #endregion
}

Dan kami menggunakannya seperti ini:

using(new WaitCursor())
{
    // very long task
}

Mungkin bukan desain terhebat, tapi ini berhasil =)

Carlo
sumber
3
Selamat menggunakan IDisposable! Cara yang baik untuk memastikan kita selalu kembali ke kursor sebelumnya.
Xavier Poinas
2
Saya punya ide yang persis sama beberapa waktu lalu. Tapi saya membungkus kode sebagai kelas privat di dalam kelas fasad layanan UI, dan mengembalikan contoh melalui metode "ShowWaitCursor". Jadi, Anda harus melakukan: using(uiServices.ShowWaitCursor()). Tampak rumit tetapi memudahkan pengujian unit.
Konamiman
sedikit offtopic: bagaimana menerapkan buang dengan benar ... msdn.microsoft.com/en-us/library/ms244737.aspx
Michael Sander
2
@AnoushkaSeechurn WaitCursor () bukan sebuah metode, itu konstruktor. Saya menduga Anda mengganti nama kelas menjadi sesuatu yang lain selain 'WaitCursor'?
Carlo
1
@ JánosTigyi Saya suka memisahkan metode antarmuka di wilayah mereka sendiri. Pertanyaannya, mengapa hack tidak menggunakan wilayah di sana? = P
Carlo
40

Saya menggunakan jawaban di sini untuk membangun sesuatu yang bekerja lebih baik untuk saya. Masalahnya adalah ketika blok penggunaan dalam jawaban Carlo selesai, UI mungkin sebenarnya masih sibuk dalam penyatuan data. Mungkin ada data yang lambat dimuat atau peristiwa yang diaktifkan sebagai akibat dari apa yang telah dilakukan di blok. Dalam kasus saya, terkadang perlu beberapa detik dari kursor tunggu menghilang hingga UI benar-benar siap. Saya menyelesaikannya dengan membuat metode pembantu yang mengatur kursor tunggu dan juga mengatur pengaturan waktu yang secara otomatis akan mengatur kursor kembali ketika UI sudah siap. Saya tidak dapat memastikan bahwa desain ini akan berfungsi di semua kasus, tetapi berhasil untuk saya:

    /// <summary>
    ///   Contains helper methods for UI, so far just one for showing a waitcursor
    /// </summary>
    public static class UiServices
    {

    /// <summary>
    ///   A value indicating whether the UI is currently busy
    /// </summary>
    private static bool IsBusy;

    /// <summary>
    /// Sets the busystate as busy.
    /// </summary>
    public static void SetBusyState()
    {
        SetBusyState(true);
    }

    /// <summary>
    /// Sets the busystate to busy or not busy.
    /// </summary>
    /// <param name="busy">if set to <c>true</c> the application is now busy.</param>
        private static void SetBusyState(bool busy)
        {
            if (busy != IsBusy)
            {
                IsBusy = busy;
                Mouse.OverrideCursor = busy ? Cursors.Wait : null;

                if (IsBusy)
                {
                    new DispatcherTimer(TimeSpan.FromSeconds(0), DispatcherPriority.ApplicationIdle, dispatcherTimer_Tick, Application.Current.Dispatcher);
                }
            }
        }

        /// <summary>
        /// Handles the Tick event of the dispatcherTimer control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private static void dispatcherTimer_Tick(object sender, EventArgs e)
        {
                var dispatcherTimer = sender as DispatcherTimer;
                if (dispatcherTimer != null)
                {
                    SetBusyState(false);
                    dispatcherTimer.Stop();
                }
        }
    }
TJKjaer
sumber
+1 kode yang sangat bagus, bagaimana Anda menerapkan yang sama untuk C # Winforms. sebagai gantinya: Cursor.Current = Cursors.WaitCursor; // busy Cursor.Current = Cursors.Default;
Zeeshanef
+1 Desain yang sangat bersih, mudah dan otomatis. Jika Anda mencari solusi aplikasi yang sibuk, gunakan kode TJ. Letakkan di kelas Helper dan gunakan seperti ini sebelum menjalankan operasi yang panjang: Helpers.UiServices.SetBusyState ();
Aaron Reed
14

Saya hanya melakukan

Mouse.OverrideCursor = Cursors.Wait;
try {
    // Long lasting stuff ...
} finally {
    Mouse.OverrideCursor = null;
}

Menurut dokumentasi Properti Mouse.OverrideCursor

Untuk menghapus kursor pengganti, setel OverrideCursor ke nol.

Pernyataan coba-akhirnya memastikan bahwa kursor default dipulihkan dalam hal apa pun, bahkan ketika pengecualian terjadi atau bagian percobaan dibiarkan dengan returnatau break(jika di dalam loop).

Olivier Jacot-Descombes
sumber
Tugas kerja yang lama dapat menimbulkan pengecualian karena berbagai alasan. Ini adalah pendekatan yang bagus
aggsol
6

Cara terbaik adalah dengan tidak menyebabkan UI menjadi non-responsif, memindahkan semua pekerjaan ke utas / tugas lain yang sesuai.

Selain itu, Anda agak menangkap-22: jika Anda menambahkan cara untuk mendeteksi bahwa ui tidak responsif, tidak ada cara yang baik untuk mengubah kursor, sebagai tempat Anda perlu melakukan itu ( benang genap) tidak responsif ... Anda mungkin dapat menggunakan pinvoke ke kode win32 standar untuk mengubah kursor ke seluruh jendela?

Jika tidak, Anda harus melakukannya terlebih dahulu, seperti yang disarankan pertanyaan Anda.

John Gardner
sumber
5

Saya pribadi lebih suka untuk tidak melihat pointer mouse beralih berkali-kali dari jam pasir ke panah. Untuk membantu mencegah perilaku tersebut saat memanggil fungsi tertanam yang membutuhkan waktu beberapa saat dan masing-masing mencoba untuk mengontrol penunjuk mouse, saya menggunakan tumpukan (penghitung) yang saya sebut LifeTrackerStack. Dan hanya ketika tumpukan kosong (penghitung ke 0) saya mengatur kembali jam kaca ke panah.

Saya juga menggunakan MVVM. Saya juga lebih suka kode aman utas.

Di kelas dasar model saya, saya mendeklarasikan LifeTrackerStack saya bahwa saya mengisi di kelas model anak-anak atau menggunakan langsung dari kelas model anak ketika saya memiliki akses ke sana dari mereka.

Pelacak hidup saya memiliki 2 keadaan / tindakan:

  • Alive (counter> 0) => ubah Model.IsBusy menjadi true;
  • Selesai (counter == 0) => ubah Model.IsBusy menjadi false;

Kemudian dalam pandangan saya, saya mengikat secara manual ke Model.IsBusy saya dan melakukan:

void ModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == "IsBusy")
    {
        if (this._modelViewAnalysis.IsBusy)
        {
            if (Application.Current.Dispatcher.CheckAccess())
            {
                this.Cursor = Cursors.Wait;
            }
            else
            {
                Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = Cursors.Wait));
            }
        }
        else
        {
            Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = null));
        }
    }

Ini adalah kelas saya LifeTrackerStack:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;

namespace HQ.Util.General
{
    /// <summary>
    /// Usage is to have only one event for a recursive call on many objects
    /// </summary>
    public class LifeTrackerStack
    {
        // ******************************************************************
        protected readonly Action _stackCreationAction;
        protected readonly Action _stackDisposeAction;
        private int _refCount = 0;
        private object _objLock = new object();
        // ******************************************************************
        public LifeTrackerStack(Action stackCreationAction = null, Action stackDisposeAction = null)
        {
            _stackCreationAction = stackCreationAction;
            _stackDisposeAction = stackDisposeAction;
        }

        // ******************************************************************
        /// <summary>
        /// Return a new LifeTracker to be used in a 'using' block in order to ensure reliability
        /// </summary>
        /// <returns></returns>
        public LifeTracker GetNewLifeTracker()
        {
            LifeTracker lifeTracker = new LifeTracker(AddRef, RemoveRef);

            return lifeTracker;
        }

        // ******************************************************************
        public int Count
        {
            get { return _refCount; }
        }

        // ******************************************************************
        public void Reset()
        {
            lock (_objLock)
            {
                _refCount = 0;
                if (_stackDisposeAction != null)
                {
                    _stackDisposeAction();
                }
            }
        }

        // ******************************************************************
        private void AddRef()
        {
            lock (_objLock)
            {
                if (_refCount == 0)
                {
                    if (_stackCreationAction != null)
                    {
                        _stackCreationAction();
                    }
                }
                _refCount++;
            }
        }

        // ******************************************************************
        private void RemoveRef()
        {
            bool shouldDispose = false;
            lock (_objLock)
            {
                if (_refCount > 0)
                {
                    _refCount--;
                }

                if (_refCount == 0)
                {
                    if (_stackDisposeAction != null)
                    {
                        _stackDisposeAction();
                    }
                }
            }
        }

        // ******************************************************************
    }
}


using System;

namespace HQ.Util.General
{
    public delegate void ActionDelegate();

    public class LifeTracker : IDisposable
    {
        private readonly ActionDelegate _actionDispose;
        public LifeTracker(ActionDelegate actionCreation, ActionDelegate actionDispose)
        {
            _actionDispose = actionDispose;

            if (actionCreation != null)
                actionCreation();
        }

        private bool _disposed = false;
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this._disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    _actionDispose();
                }

                // Note disposing has been done.
                _disposed = true;
            }
        }
    }
}

Dan penggunaannya:

    _busyStackLifeTracker = new LifeTrackerStack
        (
            () =>
            {
                this.IsBusy = true;
            },
            () =>
            {
                this.IsBusy = false;
            }
        );

Di mana pun saya melakukan jogging yang lama, saya melakukan:

        using (this.BusyStackLifeTracker.GetNewLifeTracker())
        {
            // long job
        }

Ini bekerja untuk saya. Semoga bisa membantu! Eric

Eric Ouellet
sumber
@ menit, Terima kasih banyak. Dengan pujian yang bagus seperti itu, saya harus memposting solusi sebenarnya yang saya gunakan. Mungkin saja ada beberapa gangguan (saya tidak dapat menyertakan semua kerangka kerja saya) yang semestinya mudah diperbaiki (mungkin harus menghapus beberapa kode). Saya menambahkan jawaban baru dengan kode yang lebih baik (menurut saya) dan lebih lengkap yang sesuai dengan lebih banyak situasi. Anda bisa melihatnya jika mau. ;-)
Eric Ouellet
3

Hati-hati di sini karena mengutak-atik Wait Cursor dapat menyebabkan beberapa masalah dengan utas STA. Pastikan bahwa jika Anda menggunakan hal ini, Anda melakukannya di utasnya sendiri. Saya memposting contoh di sini Jalankan di dalam STA yang menggunakan ini untuk menunjukkan WaitCursor saat file yang muncul sedang dimulai, dan tidak meledakkan AFAICT (aplikasi utama).

AllenM
sumber
1

Mengubah kursor tidak berarti aplikasi tidak akan merespons peristiwa mouse dan keyboard setelah tugas yang berjalan lama selesai. Untuk menghindari kesalahan pengguna, saya menggunakan kelas di bawah ini yang menghapus semua pesan keyboard dan mouse dari antrian pesan aplikasi.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;

public class WpfHourGlass : IDisposable
{

    [StructLayout(LayoutKind.Sequential)]
    private struct POINTAPI
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MSG
    {
        public int hwnd;
        public int message;
        public int wParam;
        public int lParam;
        public int time;
        public POINTAPI pt;
    }
    private const short PM_REMOVE = 0x1;
    private const short WM_MOUSELAST = 0x209;
    private const short WM_MOUSEFIRST = 0x200;
    private const short WM_KEYFIRST = 0x100;
    private const short WM_KEYLAST = 0x108;
    [DllImport("user32", EntryPoint = "PeekMessageA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
    private static extern int PeekMessage([MarshalAs(UnmanagedType.Struct)]
    ref MSG lpMsg, int hwnd, int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);

    public WpfHourGlass()
    {
        Mouse.OverrideCursor = Cursors.Wait;
        bActivated = true;
    }
    public void Show(bool Action = true)
    {
        if (Action)
        {
            Mouse.OverrideCursor = Cursors.Wait;
        }
        else
        {
            Mouse.OverrideCursor = Cursors.Arrow;
        }

        bActivated = Action;

    }
    #region "IDisposable Support"
    // To detect redundant calls
    private bool disposedValue;
    private bool bActivated;
    // IDisposable
    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposedValue)
        {
            if (disposing)
            {
                //remove todas as mensagens de mouse
                //e teclado que tenham sido produzidas
                //durante o processamento e estejam
                //enfileiradas
                if (bActivated)
                {
                    MSG pMSG = new MSG();
                    while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)))
                    {
                    }
                    while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE)))
                    {
                    }
                    Mouse.OverrideCursor = Cursors.Arrow;

                }
            }

            // TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
            // TODO: set large fields to null.
        }
        this.disposedValue = true;
    }

    public void Dispose()
    {
        // Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion

}
pengguna1780087
sumber
1

Saya menggunakan solusi Olivier Jacot-Descombes, sangat sederhana dan berfungsi dengan baik. Terima kasih. update: bahkan bekerja dengan baik tanpa menggunakan threads / background worker yang berbeda.

Saya menggunakannya dengan backgroudworker, kursor mouse terlihat bagus saat sibuk bekerja dan kembali normal saat pekerjaan selesai.

public void pressButtonToDoSomeLongTimeWork()
{    
    Mouse.OverrideCursor = Cursors.Wait;
    // before the long time work, change mouse cursor to wait cursor

    worker.DoWork += doWorkLongTimeAsync;
    worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    worker.RunWorkerAsync();  //start doing some long long time work but GUI can update
}

private void worker_RunWorkerCompleted(object sender,     
                                       RunWorkerCompletedEventArgs e)
{
    //long time work is done();
    updateGuiToShowTheLongTimeWorkResult();
    Mouse.OverrideCursor = null;  //return mouse cursor to normal
}
ntchris
sumber
0

Saya tahu saya terlambat, saya baru saja mengubah cara saya mengelola kursor Hourglass (status sibuk) aplikasi saya.

Solusi yang diusulkan ini lebih kompleks daripada jawaban pertama saya, tetapi menurut saya ini lebih lengkap dan lebih baik.

Saya tidak mengatakan saya memiliki solusi yang mudah atau lengkap. Tetapi bagi saya ini adalah yang terbaik karena sebagian besar memperbaiki semua masalah yang saya alami saat mengelola status sibuk aplikasi saya.

Keuntungan:

  • Dapat mengelola status "Sibuk" baik dari model maupun tampilan.
  • Mampu mengelola status sibuk baik jika tidak ada GUI. Dipisahkan dari GUI.
  • Aman benang (dapat digunakan dari utas apa pun)
  • Mendukung Busy override (tampilkan panah sementara) ketika ada Window (Dialog) yang seharusnya terlihat di tengah-tengah transaksi yang sangat panjang.
  • Mampu menumpuk banyak operasi dengan perilaku sibuk yang menunjukkan jam pasir konstan baik jika itu merupakan bagian dari banyak sub tugas yang sedikit panjang. Memiliki jam pasir yang tidak sering berubah dari sibuk ke normal menjadi sibuk. Status sibuk konstan jika memungkinkan, dengan menggunakan tumpukan.
  • Mendukung langganan acara dengan penunjuk lemah karena contoh objek "Global" bersifat global (tidak akan pernah Dikumpulkan Sampah - di-root).

Kode tersebut dipisahkan menjadi beberapa kelas:

  • Tidak ada kelas GUI: "Global" yang mengelola status Sibuk dan harus diinisialisasi saat aplikasi dimulai dengan petugas operator. Karena bersifat Global (tunggal), saya memilih untuk memiliki acara NotifyPropertyChanged yang lemah agar tidak menahan siapa pun yang ingin diberi tahu jika ada perubahan.
  • Kelas GUI: AppGlobal yang menghubungkan ke Global dan mengubah tampilan Mouse sesuai dengan status Gloab.Busy. Ini harus diinisialisasi dengan dispatcher juga saat program dimulai.
  • Kelas GUI untuk membantu Dialog (Window) agar memiliki perilaku mouse yang tepat saat digunakan dalam transaksi panjang di mana mouse telah diganti untuk menampilkan jam pasir dan menginginkan panah reguler saat Dialog (Window) sedang digunakan.
  • Kode tersebut juga menyertakan beberapa dependensi.

Inilah kegunaannya:

Init:

public partial class App : Application
{
    // ******************************************************************
    protected override void OnStartup(StartupEventArgs e)
    {
        Global.Init(Application.Current.Dispatcher);
        AppGlobal.Init(Application.Current.Dispatcher);

Penggunaan yang disukai:

        using (Global.Instance.GetDisposableBusyState())
        {
        ...
        }

Penggunaan lainnya:

// ******************************************************************
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);
}

Kursor Jendela Otomatis:

public partial class DlgAddSignalResult : Window
{
    readonly WindowWithAutoBusyState _autoBusyState = new WindowWithAutoBusyState();

    // ******************************************************************
    public DlgAddSignalResult()
    {
        InitializeComponent();

        Model = DataContext as DlgAddSignalResultModel;

        _autoBusyState.Init(this);
    }

Kode:

// Copyright (c) 2008 Daniel Grunwald
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

/* Usage:
 * 
 * 
 * 
        public delegate void FileChangedHandler(object sender, FileSystemEventArgs e);

        [field: NonSerialized]
        private readonly SmartWeakEvent<FileChangedHandler> _weakFileChanged = new SmartWeakEvent<FileChangedHandler>();

        public event FileChangedHandler FileChanged
        {
            add
            {
                _weakFileChanged.Add(value);
            }
            remove
            {
                _weakFileChanged.Remove(value);
            }
        }
 *
 *
 */


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace HQ.Util.General.WeakEvent
{
    /// <summary>
    /// A class for managing a weak event.
    /// </summary>
    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>();

        // EO: Added for ObservableCollectionWeak
        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;

// using System.Windows.Forms;

// using Microsoft.CSharp.RuntimeBinder;

// ATTENTION: Can only be used with Framework 4.0 and up

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()) // Can be null in case of Console app
                {
                    SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
                }
                else
                {
                    Dispatcher.BeginInvoke(
                        new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
                }
            }
            catch (TaskCanceledException ex) // Prevent MT error when closing app...
            {
                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()) // Can be null in case of Console app
                {
                    SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
                }
                else
                {
                    Dispatcher.BeginInvoke(new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
                }
            }
            catch (TaskCanceledException ex) // Prevent MT error when closing app...
            {
                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
{
    /// <summary>
    /// This is the centralized calss to obtain the Dispatcher in order to make sure to get the appropriate one (the MainWindow dispatcher).
    /// If you need a specidifc thread dispatcher, please refer to System.Windows.Threading.Dispatcher or Dispatcher.CurrentDispatcher.
    /// This dispatcher should be set at the initialization (start) of any GUI executable: ex: Global.Instance.Dispatcher = Application.Current.Dispatcher;

    /// </summary>
    public class Global : NotifyPropertyChangedThreadSafeAsyncWeakBase
    {
        public delegate void IsBusyChangeHandler(bool isBusy);

        /// <summary>
        /// This event happen only the UI thread in low priority
        /// </summary>
        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);
        }

        // ******************************************************************
        /// <summary>
        /// Will set busy state temporary until object is disposed where 
        /// the state will be back to its previous state.
        /// </summary>
        /// <param name="isBusy"></param>
        /// <returns></returns>
        public LifeTracker GetDisposableBusyState(bool isBusy = true)
        {
            return new LifeTracker(() => PushState(isBusy), PullState);
        }

        // ******************************************************************
        private bool _isBusy;

        /// <summary>
        /// This property should be use by the GUI part in order to control the mouse cursor
        /// </summary>
        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();
        // ******************************************************************
        /// <summary>
        /// Always prefer usage of "Using(Global.Instance.GetDisposableBusyState())" whenever possible.
        /// Otherwise ensure to call Pull State to get back previous state of the cursor when action is 
        /// completed
        /// </summary>
        /// <param name="isBusy"></param>
        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;

        /// <summary>
        /// Only kept for historical reason / compatibility with previous code
        /// </summary>
        [Obsolete("Use direct 'using(Gloabl.Instance.GetDisposableBusyState(isBusy))' which is simpler")]
        public LifeTrackerStack BusyLifeTrackerStack
        {
            get { return _busyLifeTrackerStack; }
        }

        // ******************************************************************
        // private int _latestVersionExecuted = 0;
        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;

        /// <summary>
        /// It will record all action once per key and it
        /// once per Dispatcher queue roll over (on ContextIdle).
        /// When the dispatcher reach the DispatcherPriority.ContextIdle, it will
        /// run all action once.
        /// Thread safe... no action will be lost but can be run twice or more if
        /// some are added by other thread(s) at the same time one is executed.
        /// </summary>
        /// EO: sorry for the name but it is the best found
        /// <param name="key"></param>
        /// <param name="action"></param>
        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)
                {
                    // _latestVersionExecuted = 0;
                    _currentVersionRequired = 0;
                }
                else
                {
                    // _latestVersionExecuted = versionExecute;
                }
            }
        }

        // ******************************************************************
    }
}

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
{
    /// <summary>
    /// Ensure window cursor is "normal" (arrow) when visible.
    /// Usage: In Window class, define a member: OverrideCursorMtForWindow. Instantiate in constructor after Initialisation.
    /// </summary>
    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;
            }
        }

        // ******************************************************************

    }
}
Eric Ouellet
sumber