Most convenient use of BusyIndicator

Jan 15, 2013 at 10:14 AM
Edited Jan 15, 2013 at 10:18 AM

Hello amazing people.

So I have a database-ish application that has a BusyIndicator on the MainWindow. I've started to pick a few time-consuming processes and started singeling them out like so (currently in the MainWindow's code-behind):

 

    '**** Background-Process-Starters *****'

    Private Sub LoadDataStart()
        MyBusyIndicator.BusyContent = "Lade Zwischenspeicher..."
        RunBackGroundProcess(MyBusyIndicator, New System.ComponentModel.DoWorkEventHandler(AddressOf todo.LoadCaches), New System.ComponentModel.RunWorkerCompletedEventHandler(AddressOf LoadDataDone), 0)
    End Sub

    Public Sub ImportDataStart()
        MyBusyIndicator.BusyContent = "Importiere Daten..."
        RunBackGroundProcessWithProgress(MyBusyIndicator, New System.ComponentModel.DoWorkEventHandler(AddressOf todo.ImportFromTodo), New System.ComponentModel.RunWorkerCompletedEventHandler(AddressOf ImportDataDone), New System.ComponentModel.ProgressChangedEventHandler(AddressOf UpdateImportProgress))
    End Sub

    Public Sub ImportDataToArchiveStart()
        MyBusyIndicator.BusyContent = "Importiere Daten ins Archiv..."
        RunBackGroundProcessWithProgress(MyBusyIndicator, New System.ComponentModel.DoWorkEventHandler(AddressOf todo.ImportFromTodo), New System.ComponentModel.RunWorkerCompletedEventHandler(AddressOf ImportDataToArchiveDone), New System.ComponentModel.ProgressChangedEventHandler(AddressOf UpdateImportProgressArchive))
    End Sub


    '*** Background-Process-Reporters *******

    Public Sub UpdateImportProgress(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs)
        'do some formating stuff....
        MyBusyIndicator.BusyContent = sStatus.ToString
    End Sub

    Public Sub UpdateImportProgressArchive(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs)
        'Ditto...
    End Sub


    '********** Background-Process-Enders ********

    Private Sub LoadDataDone(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs)
        '...
        MyBusyIndicator.IsBusy = False
    End Sub

    Private Sub ImportDataDone()
         'ditto...
        MyBusyIndicator.IsBusy = False
    End Sub

    Private Sub ImportDataToArchiveDone()
         'you get the gist of it...
        MyBusyIndicator.IsBusy = False
    End Sub

 

While having this in a module (or static class):

 

    Public Sub RunBackGroundProcess(usingBI As Xceed.Wpf.Toolkit.BusyIndicator, ProcessToRun As [Delegate], WorkDoneHandler As [Delegate], Optional iDisplayAfter As Integer = 1)
        Dim worker As New ComponentModel.BackgroundWorker()
        AddHandler worker.DoWork, ProcessToRun
        AddHandler worker.RunWorkerCompleted, WorkDoneHandler
        usingBI.DisplayAfter = New TimeSpan(0, 0, iDisplayAfter)
        usingBI.IsBusy = True
        worker.RunWorkerAsync()
    End Sub

 

That was the most "generic" approach i could come up with. Of course that is just uuugly beyond reasoning. I especially don't want the first part to begin cluttering out in potentially hundreds (or tenths ;)) of Starter - Ender - Progress calls.

The functionality i (everybody?) ideally wants is: 

I have a class (databaseBackendStuff) where I have potentially long processes lurking - especially on the target machines. Can I prepare this class in some way (Inherit MyBackgroundProcessClass) that I can without much pain single out those processes and just... i don't know... set the UI to "busy" or tell the UI to call that process in a BGWorker instead?

That would - in combination with the ingenious DisplayAfter Property - be the most beautiful thing I've ever seen... I'm talking double rainbow material... all the way.

I think I might almost be there in my thought process, but the threading stuff is just scaring me and I WANT this so bad but I DO NOT HAVE TIME to debug anymore... the release candidate has to be out the day before yesterday (three months, actually). A gentle nudge in the right direction should be sufficient and would be extremely appreciated. :)

 

Thanks a lot

Christoph

Jan 19, 2013 at 10:59 AM

Hi... it's me again answering my own question... maybe I am the only one in the world who understands what it is that i want?? :)

My goal was to have a generic approach to run long-running processes (abbreviated lrp in the following) that will possibly block the UI from any place of my code and lock the main window with the BusyIndicator. What i wanted was:

- possibility for custom progress reporting
- an initial message in the BusyIndicator
- adjust the DisplayAfter TimeSpan on the fly
   (when i "know" a lrp is not supposed to take longer than 2 seconds I can make a judgement call to rather block the UI for a brief moment than "flash" the BusyIndicator for the fraction of a second)
- the ui should not have to be aware of calling a lrp so the call should be indistinguishable from a non-lrp call
   not the ui "decides" what is defined as a lrw, but the code-behind 

  - as little code-overhead as possible because i want to make HEAVY use of this wonderful thing and have no time!

I am very happy with the following solution and even more happy to share it with whomever it may be useful to - i think it conveys an important overall structure of how this works and can easily be adapted. I tried to make it as readable as possible without wrecking my code which is due toworrow (lying: three months ago).

In my main application (called todo) window (accessed by the Property MyMainWindow) i have a BusyIndicator (called MyBusyIndicator) and the following code:

    Private WithEvents bgw As BackgroundWorker

    Public Function StartBackgroundWorker(InitialBusyContent As Object, WorkToDo As [Delegate], Optional WorkArgument As Object = Nothing, Optional DisplayAfter As TimeSpan = Nothing) As BackgroundWorker
        'I can only do one thing at a time so please stop bothering me!!
        If bgw IsNot Nothing AndAlso bgw.IsBusy Then
            Throw New Exception("A long running process is already in progress")
        End If

        'Initialize Backgroundworker
        bgw = New BackgroundWorker()
        MyBusyIndicator.DisplayAfter = IIf(DisplayAfter.Ticks = 0, TimeSpan.FromSeconds(1), DisplayAfter)
        MyBusyIndicator.BusyContent = InitialBusyContent
        MyBusyIndicator.IsBusy = True
        bgw.WorkerReportsProgress = True

        'Set the work process and gogogo
        AddHandler bgw.DoWork, WorkToDo
        bgw.RunWorkerAsync(WorkArgument)

        'Returning the worker seemed clever for whatever reason
        Return bgw
    End Function

    Public Sub BackgroundWorkerDone(sender As Object, e As ComponentModel.RunWorkerCompletedEventArgs) Handles bgw.RunWorkerCompleted
        'thy work is done
        MyBusyIndicator.IsBusy = False
    End Sub

    Public Sub UpdateBusyProgress(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles bgw.ProgressChanged
        'show all kinds of fancy progress
        MyBusyIndicator.BusyContent = e.UserState
    End Sub

This could easily be changed to optionally accept a new BGW every time it is called and use AddHandler for the UI update - i choose to only use one because the long running processes are always called from the ui and the ui is blocked while they are running, so there cannot possibly be two processes running concurrently.

Whenever i have a process that could possibly be long running I will declare it as a private Sub as follows:

    Private Sub _HoldYourBreath(sender As Object, e As ComponentModel.DoWorkEventArgs)
        'sender is the BackgroundWorker, kindly provided by the UI
        'tha actual Argument(s) for this call are wrapped in the e.Argument Object
        'which could also be a ParamArray for convenience

        Dim totalSteps As Integer = e.Argument

        For i As Integer = -20 To totalSteps
            System.Threading.Thread.Sleep(100)
            If i > 0 Then
                'report counting progress to ui by calling sender's ReportProgress Method 
                'and pass my custom progress message (which could be a Framework Element)
                CType(sender, ComponentModel.BackgroundWorker).ReportProgress((i / totalSteps) * 100, "Counting ... " & i)
            End If
        Next

    End Sub

And wrap it in this Public caller:

    Public Sub HoldYourBreath(countto As Integer)
        'start the progress in the background
        todo.MyMainWindow.StartBackgroundWorker("OK, I will hold my breath and count to " & countto, New ComponentModel.DoWorkEventHandler(AddressOf _HoldYourBreath), countto)
    End Sub

This gives me all the functionality i want with only 2 steps of overhead compared to a non-lrp: I have to

- wrap lrp call's arguments in an object, but this is done privately in the lrp class and basically just in O(n) lines of code per argument (1 line for the wrapping, 1 for the unwrapping)
- wrap the actual lrp call (basically 1 line of extracode)

So i am well within a couple of minutes of additional work to transform any possibly ui-blocking LRP into a clean, nifty, responsive "please wait" screen with custom, arbitrary progress monitoring. That is beyond excellent.

I have experimented a little with derivating the LRP-caller-class from a Class (something like MustInherit Class callsLongRunningProcesses) trying to wrap the process even more and essentially creating an interface for calling LRPs. But halfway down the road i realized that the coding overhead per LRP does not seem to get any smaller than this, and as I am the only programmer ever to work on this project I am perfectly happy with my current solution.

 

Best regards

Christoph