How to properly implement Busy Indicator from WPF Extended Toolkit

14,162

Well, I finally have the busy indicator implemented. It's been quite the arduous task, but I've learned a bit along the way. At least I have something to show for it. While I didn't figure out why my code wasn't working as I have it posted, I did find that I was trying to execute UI related code in the action that would certainly be a problem. I'm not 100% sure because I did a bit of a re-write and I'm not froggy enough to revert my code to play with it. But, the question is answered and here is how I implemented the busy indicator:

First, nothing changed in my view except the property set and binding for IsBusy can be removed. I ended up moving my event subscription from the view model and into the code behind of the view. My view's code behind looks like this:

public partial class Shell : Window
{
    #region Constructor(s)

    public Shell()
    {
        InitializeComponent();
        StateManager.IsBusyChange += new StateManager.IsBusyHandler(InitiateIsBusy);
    }

    #endregion Constructor(s)

    #region Event Actions

    public void InitiateIsBusy(object sender, BusyActionEventArgs e)
    {
        BackgroundWorker worker = new BackgroundWorker();

        worker.DoWork += (o, ea) =>
        {
            e.IsBusyAction.Invoke();
            //Dispatcher.Invoke((Action)(() => e.IsBusyAction()));
        };
        worker.RunWorkerCompleted += (o, ea) =>
        {
            this.ShellBusyIndicator.IsBusy = false;
        };

        this.ShellBusyIndicator.BusyContent = e.BusyMessage;
        this.ShellBusyIndicator.IsBusy = true;

        worker.RunWorkerAsync();
    }

    #endregion Event Actions
}

First, note that I created the BusyActionEventArgs class that extends EventArgs. It is placed into my Library project which is a dependency on every project in my solution:

public class BusyActionEventArgs : EventArgs
{
    public Action IsBusyAction { get; set; }
    public string BusyMessage { get; set; }
}

Last, my StateManager now looks like this:

public static class StateManager
{
    public static void IsBusyProcess(Action action)
    {
        IsBusyProcess(action, "Processing...");
    }

    public static void IsBusyProcess(Action action, String isBusyMessage)
    {
        BusyActionEventArgs args = new BusyActionEventArgs()
        {
            IsBusyAction = action,
            BusyMessage = isBusyMessage
        };

        IsBusyChange(null, args);
    }

    public delegate void IsBusyHandler(object sender, BusyActionEventArgs e);

    public static event IsBusyHandler IsBusyChange;
}

Note that I created an overload for the IsBusyProcess method so I can send in a custom Busy Message if I'd like.

Now if I modify the code from my question's code snippet where I am actually using the Busy Indicator, it looks like this:

StateManager.IsBusyProcess(() =>
        {
            this.Validate();

            if (!HasValidationErrors)
            {
                if (this.CustomerControlVM.SaveCustomer() != 0)
                {
                    VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
                    this.VehicleControlVM.SaveVehicle();
                }
            }
        });

        ComplaintsView complaintsControl = new ComplaintsView();

        (complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
        (complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
        StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);

Note the code I removed from the action's scope. This code is UI code and will cause an error if it is inside the scope.

I hope this helps someone else who might want to implement something similar or is having troubles. Thanks stackoverflow for just existing on this one. Sometimes talking (or typing) it out makes the difference.

Share:
14,162
flyNflip
Author by

flyNflip

Updated on June 04, 2022

Comments

  • flyNflip
    flyNflip almost 2 years

    Ok, I have been struggling with this issue for quite some time and have read article after article trying to get an idea on the issue. I am trying to implement the Busy Indicator and am only partially successful. I have a Shell view that I have wrapped with the BusyIndicator like so:

    <Window x:Class="Foundation.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:Library.Controls.Views;assembly=Library"
        xmlns:l="clr-namespace:Library.StaticClasses"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        Name="ShellView"
        Style="{StaticResource BusyWindowStyle}"
        SourceInitialized="Window_SourceInitialized"
        DataContext="{StaticResource ShellVM}">
    
        <xctk:BusyIndicator x:Name="ShellBusyIndicator" IsBusy="{Binding IsBusy}"  DisplayAfter="0">
            <Grid>
                <!--Content Here -->
            </Grid>
        </xctk:BusyIndicator>
    

    Following the MVVM pattern, I have a ShellViewModel that looks like so:

    public class ShellViewModel : ViewModelBase
    {
        #region constructor(s)
    
        public ShellViewModel()
        {
            StateManager.IsBusyChange += new StateManager.IsBusyHandler(IsBusyEventAction);
        }
    
        #endregion constructor(s)
    
        #region properties
    
        private bool _IsBusy;
        public bool IsBusy
        {
            get
            {
                return _IsBusy;
            }
            set
            {
                if (_IsBusy != value)
                {
                    _IsBusy = value;
                    OnPropertyChanged("IsBusy");
                }
            }
        }
    
        private string _IsBusyMessage;
        public string IsBusyMessage
        {
            get
            {
                return _IsBusyMessage;
            }
            set
            {
                if (_IsBusyMessage != value)
                {
                    _IsBusyMessage = value;
                    OnPropertyChanged("IsBusyMessage");
                }
            }
        }
    
        #endregion properties
    
        #region actions, functions, and methods
    
        private void IsBusyEventAction(object sender, EventArgs e)
        {
            if (StateManager.IsBusy)
            {
                this.IsBusy = true;
                this.IsBusyMessage = StateManager.IsBusyMessage;
            }
            else
            {
                this.IsBusy = false;
            }
        }
    
        #endregion actions, functions, and methods
    }
    

    Next, I am using a static class called StateManager that I can call from anywhere that will trigger the busy indicator:

    public static class StateManager
    {
        public static String IsBusyMessage { get; set; }
    
        public static void IsBusyProcess(Action action)
        {
            IsBusyProcess(action, "Processing...");
        }
    
        public static void IsBusyProcess(Action action, String isBusyMessage)
        {
            IsBusyMessage = isBusyMessage;
            IsBusy = true;
            Application.Current.Dispatcher.Invoke(action, System.Windows.Threading.DispatcherPriority.Background);
            IsBusy = false;
        }
    
        private static bool _IsBusy;
        public static bool IsBusy
        {
            get
            {
                return _IsBusy;
            }
            set
            {
                if (_IsBusy != value)
                {
                    _IsBusy = value;
    
                    IsBusyChange(null, null);
                }
            }
        }
    
        public delegate void IsBusyHandler(object sender, EventArgs e);
    
        public static event IsBusyHandler IsBusyChange;
    }
    

    Finally, I am triggering the busy indicator in code like so:

    StateManager.IsBusyProcess(() =>
            {
                this.Validate();
    
                if (!HasValidationErrors)
                {
                    if (this.CustomerControlVM.SaveCustomer() != 0)
                    {
                        VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
                        this.VehicleControlVM.SaveVehicle();
    
                        ComplaintsView complaintsControl = new ComplaintsView();
    
                        (complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
                        (complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
                        StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
                    }
                }
    
                StateManager.IsBusy = false;
            });
    

    To summarize, the StateManager can be called from anywhere in the application. When the StateManager.IsBusy property is changed, an event, which is subscribed to in the ShellViewModel, is fired. When this happens, the IsBusy property in the ShellViewModel is set to true supposedly causing the IsBusy indicator to show. The logic is working fine in the sense that the IsBusy in the ShellViewModel is being set and the busy indicator evens shows as expected. What isn't working right is that the busy indicator does not animate. It simply stays stagnate. I feel like this is a threading issue but either my understanding of the UI thread is tilted or I am not seeing something that is hopefully obvious to you. I realize the IsBusy logic cannot be on the same thread as the UI, but I would think that since my logic is happening in the view model, it shouldn't be an issue. Am I wrong? Any ideas or thoughts? One other thing that might be helpful: I was having trouble even getting the indicator to show and created this post which was resolved: How to implement busy indicator in application shell