2010-01-18

Wskaźnik postępu testu pod Visual Studio

Kiedy nasze testy trwają coraz dłużej pojawia się potrzeba jakiegoś wskaźnika postępu testu. Żebyśmy przynajmniej wiedzieli, że coś się dzieje. Visual Studio nie ma żadnej wbudowanej możliwości do pokazywania postępu testu. Pozostaje napisać coś samemu:
public class ProgressIndicator
{
    private Thread m_thread;
    private ProgressForm m_form = new ProgressForm();

    public ProgressIndicator()
    {
        AutoResetEvent are = new AutoResetEvent(false);

        m_thread = new Thread(() =>
        {
            Application.EnableVisualStyles();
            are.Set();
            Application.Run(m_form);
            m_thread = null;
        });

        m_thread.SetApartmentState(ApartmentState.MTA);
        m_thread.Start();
        are.WaitOne();
        are.Close();
    }

    public bool IsDisposed
    {
        get
        {
            return m_form.IsDisposed;
        }
    }

    private T Invoke<T>(Func<T> a_delegate)
    {
        if (m_form.InvokeRequired)
        {
            try
            {
                return (T)m_form.Invoke(a_delegate);
            }
            catch (ObjectDisposedException)
            {
                return default(T);
            }
        }
        else if (m_form.IsHandleCreated)
            return a_delegate();
        else
            return default(T);
    }

    private void Invoke(Action a_delegate)
    {
        if (m_form.InvokeRequired)
            m_form.BeginInvoke(a_delegate);
        else if (m_form.IsHandleCreated)
            a_delegate();
    }

    public int Progress
    {
        get
        {
            return Invoke(() => { return m_form.Progress; });
        }
        set
        {
            Invoke(() => { m_form.Progress = value; });
        }
    }

    public string TestName
    {
        get
        {
            return Invoke(() => { return m_form.TestName; });
        }
        set
        {
            Invoke(() => { m_form.TestName = value; });
        }
    }
}
Klasa jest bardzo prosta. Posiada dwie istotne właściwości pozwalające na ustawianie nazwy okna i wskaźnika postępu. Kod po stronie okna wygląda tak:
public partial class ProgressForm : Form
{
    private string m_testName;

    public ProgressForm()
    {
        InitializeComponent();
    }

    public string TestName
    {
        get
        {
            return m_testName;
        }
        set
        {
            m_testName = value;
            UpdateCaption();
        }
    }

    public int Progress
    {
        get
        {
            return progressBar.Value;
        }
        set
        {
            progressBar.Value = value;
            UpdateCaption();
        }
    }

    private void UpdateCaption()
    {
        Text = m_testName + " - " + progressBar.Value + "%";
    }
}
Problemem okazał się moment zamknięcia okna postępu testu podczas jego trwania. Istnieje taka możliwość, że InvokeRequired() zwróci true, zaś wywołanie Invoke() wyjątek o tym, że okno zostało zamknięte (w kodzie metody Invoke() zanim ta metoda sprawdzi warunki poprawności wywołania okno jest zamykane). Jedyną możliwością zabezpieczenia się przed tym jest łapanie wyjątku.