แสดงนาฬิกาทรายเมื่อแอปพลิเคชันไม่ว่าง


92

สำหรับมุมมองที่สร้างโดยใช้ WPF ฉันต้องการเปลี่ยนเคอร์เซอร์ของเมาส์เป็นนาฬิกาทรายเมื่อแอปพลิเคชันไม่ว่างและไม่ตอบสนอง

ทางออกหนึ่งคือการเพิ่ม

 this.Cursor = Cursors.Wait;

ไปยังสถานที่ทั้งหมดที่อาจทำให้ UI ไม่ตอบสนอง แต่เห็นได้ชัดว่านี่ไม่ใช่ทางออกที่ดีที่สุด ฉันสงสัยว่าอะไรคือวิธีที่ดีที่สุดในการบรรลุเป้าหมายนี้

เป็นไปได้หรือไม่ที่จะบรรลุสิ่งนี้โดยใช้สไตล์หรือทรัพยากร?

ขอบคุณ

คำตอบ:


227

เราทำคลาสแบบใช้แล้วทิ้งที่เปลี่ยนเคอร์เซอร์ให้เราเมื่อแอพใช้เวลานานดูเหมือนว่า:

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
}

และเราใช้มันดังนี้:

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

อาจไม่ใช่การออกแบบที่ดีที่สุด แต่มันเป็นเคล็ดลับ =)


3
ทำได้ดีกับการใช้ IDisposable! วิธีที่ดีเพื่อให้แน่ใจว่าเรากลับไปที่เคอร์เซอร์ก่อนหน้าเสมอ
Xavier Poinas

2
เมื่อไม่นานมานี้ฉันมีความคิดเดียวกัน แต่ฉันห่อรหัสเป็นคลาสส่วนตัวภายในคลาสซุ้มบริการ UI และส่งคืนอินสแตนซ์ของโค้ดนี้ผ่านเมธอด "ShowWaitCursor" คุณต้องทำ: using(uiServices.ShowWaitCursor()). ดูยุ่งยาก แต่ช่วยลดการทดสอบหน่วย
Konamiman

a bit offtopic: วิธีการใช้ dispose อย่างถูกต้อง ... msdn.microsoft.com/en-us/library/ms244737.aspx
Michael Sander

2
@AnoushkaSeechurn WaitCursor () ไม่ใช่เมธอด แต่เป็นตัวสร้าง ฉันเดาว่าคุณเปลี่ยนชื่อคลาสเป็นอย่างอื่นที่ไม่ใช่ 'WaitCursor'?
Carlo

1
@ JánosTigyiฉันชอบแยกวิธีการเชื่อมต่อในภูมิภาคของตัวเอง คำถามคือทำไมแฮ็คไม่ใช้ภูมิภาคที่นั่น? = P
Carlo

40

ฉันใช้คำตอบที่นี่เพื่อสร้างสิ่งที่ดีขึ้นสำหรับฉัน ปัญหาคือเมื่อบล็อกการใช้งานในคำตอบของ Carlo เสร็จสิ้น UI อาจยังคงยุ่งอยู่กับการค้นหาฐานข้อมูล อาจมีการโหลดข้อมูลหรือเหตุการณ์ที่เกิดขึ้นเนื่องจากสิ่งที่ทำในบล็อก ในกรณีของฉันบางครั้งอาจใช้เวลาหลายวินาทีกว่าที่ผู้รอจะหายไปจนกว่า UI จะพร้อมใช้งานจริง ฉันแก้ไขได้โดยการสร้างวิธีการช่วยเหลือที่ตั้งค่า waitcursor และดูแลการตั้งค่าตัวจับเวลาที่จะตั้งค่าเคอร์เซอร์กลับโดยอัตโนมัติเมื่อ UI พร้อม ฉันไม่แน่ใจว่าการออกแบบนี้จะใช้ได้ในทุกกรณี แต่มันใช้ได้ผลสำหรับฉัน:

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

+1 โค้ดที่ดีมากคุณจะใช้งาน C # Winforms ได้อย่างไร แทน: Cursor.Current = Cursors.WaitCursor; // ไม่ว่าง Cursor.Current = Cursors.Default;
Zeeshanef

+1 การออกแบบที่สะอาดง่ายและเป็นอัตโนมัติ หากคุณกำลังมองหาวิธีแก้ปัญหาแอปพลิเคชันที่วุ่นวายให้ใช้รหัสของ TJ โยนมันในคลาส Helper และใช้แบบนี้ก่อนที่จะรันการทำงานที่ยาวนาน: Helpers.UiServices.SetBusyState ();
Aaron Reed

14

ฉันแค่ทำ

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

ตามเอกสารของMouse.OverrideCursor Property

หากต้องการล้างเคอร์เซอร์การลบล้างให้ตั้งค่า OverrideCursor เป็น null

คำสั่ง try-final ช่วยให้มั่นใจได้ว่าเคอร์เซอร์เริ่มต้นจะถูกเรียกคืนไม่ว่าในกรณีใด ๆ แม้ว่าจะมีข้อยกเว้นเกิดขึ้นหรือส่วนของการลองจะเหลืออยู่returnหรือbreak(หากอยู่ในลูป)


การทำงานที่ยาวนานอาจทำให้เกิดข้อยกเว้นได้ด้วยเหตุผลหลายประการ นี่เป็นแนวทางที่ดี
aggsol

6

ที่ดีที่สุดวิธีที่จะไม่ก่อให้เกิด UI ที่จะกลายเป็นที่ไม่ตอบสนองเท่าที่เคยถ่ายทั้งหมดของการทำงานเพื่อหัวข้ออื่น ๆ / งานตามความเหมาะสม

นอกเหนือจากนั้นคุณอยู่ใน catch-22: หากคุณเพิ่มวิธีตรวจสอบว่า ui ไม่ตอบสนองไม่มีวิธีที่ดีในการเปลี่ยนเคอร์เซอร์เป็นสถานที่ที่คุณต้องทำ ( เธรดคู่) ไม่ตอบสนอง ... คุณอาจปักหมุดเป็นรหัสมาตรฐาน win32 เพื่อเปลี่ยนเคอร์เซอร์สำหรับทั้งหน้าต่างได้หรือไม่?

มิฉะนั้นคุณจะต้องทำล่วงหน้าอย่างที่คำถามของคุณแนะนำ


5

ฉันชอบที่จะไม่เห็นตัวชี้เมาส์สลับหลาย ๆ ครั้งจากนาฬิกาทรายเป็นลูกศร เพื่อช่วยป้องกันพฤติกรรมดังกล่าวในขณะที่เรียกใช้ฟังก์ชันแบบฝังที่ใช้เวลาสักครู่และแต่ละครั้งพยายามควบคุมตัวชี้เมาส์ฉันใช้สแต็ก (ตัวนับ) ที่ฉันเรียกว่า LifeTrackerStack และเฉพาะเมื่อสแต็กว่างเปล่า (นับเป็น 0) ที่ฉันตั้งค่ากระจกชั่วโมงกลับเป็นลูกศร

ฉันยังใช้ MVVM ฉันชอบรหัสที่ปลอดภัยของเธรดด้วย

ในคลาสรูทของโมเดลฉันประกาศ LifeTrackerStack ของฉันว่าฉันเติมข้อมูลในคลาสโมเดลลูกหรือใช้โดยตรงจากคลาสโมเดลลูกเมื่อฉันสามารถเข้าถึงมันได้

ตัวติดตามชีวิตของฉันมี 2 สถานะ / การกระทำ:

  • Alive (ตัวนับ> 0) => เปลี่ยน Model.IsBusy เป็น true;
  • เสร็จสิ้น (ตัวนับ == 0) => เปลี่ยน Model.IsBusy เป็น false;

จากนั้นในมุมมองของฉันฉันเชื่อมโยงกับ Model.IsBusy ของฉันด้วยตนเองและทำ:

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

นี่คือ 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;
            }
        }
    }
}

และการใช้งาน:

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

ทุกที่ที่มีการเขย่าเบา ๆ เป็นเวลานานฉันจะ:

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

มันใช้ได้กับฉัน หวังว่ามันจะช่วยได้! เอริค


@ นาทีขอบคุณมาก ด้วยคำชมที่ดีฉันต้องโพสต์วิธีแก้ปัญหาจริงที่ฉันใช้ เป็นไปได้ว่ามีข้อผิดพลาดเล็กน้อย (ฉันไม่สามารถรวมเฟรมเวิร์กทั้งหมดของฉัน) ที่ควรแก้ไขได้ง่าย (อาจต้องลบโค้ดบางส่วนออก) ฉันได้เพิ่มคำตอบใหม่ด้วยรหัสที่ดีกว่า (ฉันคิดว่า) และสมบูรณ์ยิ่งขึ้นซึ่งเหมาะกับสถานการณ์มากขึ้น คุณสามารถดูได้หากต้องการ ;-)
Eric Ouellet

3

โปรดระวังที่นี่เนื่องจากการเล่นซอกับ Wait Cursor อาจทำให้เกิดปัญหากับเธรด STA ได้ ตรวจสอบให้แน่ใจว่าหากคุณใช้สิ่งนี้ที่คุณกำลังทำอยู่ภายในเธรดของตัวเอง ฉันโพสต์ตัวอย่างที่นี่เรียกใช้ภายใน STAซึ่งใช้สิ่งนี้เพื่อแสดง WaitCursor ในขณะที่ไฟล์ที่สร้างกำลังเริ่มต้นขึ้นและไม่ระเบิด (แอปพลิเคชันหลัก) AFAICT


1

การเปลี่ยนเคอร์เซอร์ไม่ได้หมายความว่าแอปพลิเคชันจะไม่ตอบสนองต่อเหตุการณ์ของเมาส์และคีย์บอร์ดหลังจากเสร็จสิ้นงานที่รันเป็นเวลานาน เพื่อหลีกเลี่ยงการพลาดของผู้ใช้ฉันใช้ชั้นเรียนด้านล่างซึ่งจะลบข้อความแป้นพิมพ์และเมาส์ทั้งหมดออกจากคิวข้อความของแอปพลิเคชัน

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

}

1

ฉันใช้โซลูชันของ Olivier Jacot-Descombes มันง่ายมากและทำงานได้ดี ขอบคุณ. อัปเดต: มันยังทำงานได้ดีโดยไม่ต้องใช้เธรด / โปรแกรมทำงานเบื้องหลังอื่น

ฉันใช้กับ backgroudworker เคอร์เซอร์ของเมาส์จะดูดีเมื่อทำงานไม่ว่างและกลับสู่สภาวะปกติเมื่อทำงานเสร็จ

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
}

0

ฉันรู้ว่าฉันมาสายฉันเพิ่งเปลี่ยนวิธีจัดการเคอร์เซอร์นาฬิกาทราย (สถานะไม่ว่าง) ของแอปพลิเคชันของฉัน

โซลูชันที่เสนอนี้ซับซ้อนกว่าคำตอบแรกของฉัน แต่ฉันคิดว่าสมบูรณ์และดีกว่า

ฉันไม่ได้บอกว่าฉันมีวิธีแก้ปัญหาที่ง่ายหรือสมบูรณ์ แต่สำหรับฉันมันเป็นวิธีที่ดีที่สุดเพราะส่วนใหญ่จะแก้ไขปัญหาทั้งหมดที่ฉันมีในการจัดการสถานะที่วุ่นวายในแอปพลิเคชันของฉัน

ข้อดี:

  • สามารถจัดการสถานะ "ไม่ว่าง" จากทั้งแบบจำลองและมุมมอง
  • สามารถจัดการสถานะไม่ว่างได้หากไม่มี 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);
    }

รหัส:

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

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

    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.