Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the trick to creating a responsive WPF UI when populating multiple ListBoxes?

I am working on a support tool that displays multiple TabItems in a TabControl. Each TabItem represents an employee, and within each of these employee Tabs there is another TabControl which contains additional TabItems. These TabItems represent Outlook folders for that employee (like "Working", "Completed", etc). Each of these folder TabItems contains a ListBox that is bound to an ObservableCollection of MailItems that pertain to that Outlook folder. These aren't huge collections - only a dozen or so items per ListBox. Although, in total, across all TabItems there could conceivably be 100 items or so.

The way I have currently built the application is that the app fires up and populates the screen with the appropriate employee tabs and sub-tabs. This process is fairly quick and I'm happy. I have created a Static Global.System.Timer that all folder TabItem's code-behind is synchronized with. So every 5 minutes the application will clear all ObserverableCollections and re-scan the Outlook folders.

The problem is that the scan process brings the application to a halt. I have tried using a BackgroundWorker to gather mail from Outlook as a background process, then pass a List<MailItem> object to a RunWorkerCompleted method that then runs a this.Dispatcher.BeginInvoke process that clears the respective ObservableCollection then adds the items from the List<MailItem> back to the ObservableCollection. I've even set this Dispatcher to the lower priority.

Despite this, the application is very clunky feeling during the scan/populate ListBox process. I am unclear on how to better design this and I admit I am somewhat new to this. I realize that clearing out each of the ObservableCollections is inefficient, but Outlook folder change events aren't particularly reliable, so I need to do a brute force re-scan every once in a while to insure all MailItems are represented.

Below is my code for the WPF control that contains the ListBox. Keep in mind that there are about 10 of these ListBox controls active at once.

// This entire UserControl is essentially a ListBox control
public partial class TicketListView : UserControl
    {
        private TicketList _ticketList; //this is the ObservableCollection object
        private Folder _folder;         // Outlook Folder

        public TicketListView(Folder folder)
        {
            InitializeComponent();

            _ticketList = this.FindResource("TicketList") as TicketList; 
            _folder = folder; 

            GlobalStatics.Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed);
        }

        private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            Refresh();
        }

        private void Refresh()
        {
            BackgroundWorker worker = new BackgroundWorker();

            worker.DoWork += new DoWorkEventHandler(worker_DoWork);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
            worker.RunWorkerAsync();
        }

        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            List<MailItem> tickets = new List<MailItem>();
            string filter = TicketMonitorStatics.TicketFilter(14);
            Items items = _folder.Items.Restrict(filter);

            try
            {
                foreach (MailItem mi in items.OfType<MailItem>())
                {
                    tickets.Add(mi);
                }
            }
            catch (System.Exception) { }

            e.Result = tickets;
        }

        private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            List<MailItem> tickets = e.Result as List<MailItem>;

            this.Dispatcher.BeginInvoke(new System.Action(delegate
                {
                    _ticketList.Clear();
                    PopulateList(tickets);
                }));

            BackgroundWorker worker = sender as BackgroundWorker;
            worker.Dispose();
        }

        private void PopulateList(List<MailItem> ticketList)
        {
            foreach (MailItem mi in ticketList)
            {
                this.Dispatcher.BeginInvoke(new System.Action(delegate
                    {
                        _ticketList.Add(mi);
                    }), System.Windows.Threading.DispatcherPriority.SystemIdle);
            }
        }
    }
like image 903
Jake Shakesworth Avatar asked Feb 14 '13 03:02

Jake Shakesworth


1 Answers

for your requirement, specially in WPF, you shouldn't be using timer or background worker to keep the view responsive. Instead you should be designing your app with MVVM pattern. MVVM is Model, View and View Model, where if there is a change in the model, the model updates the View Model, and the view model updates the view. This is done by just inheriting a "INotifyPropertyChanged" Interface.

Here is a simple example

Xaml part:

<Window x:Class="SimpleMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="259" Width="445">
    <Grid Margin="0,0,2,-3">
        <Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="10,33,0,0" VerticalAlignment="Top" Width="75"/>
        <Label x:Name="label" Content="{Binding Name}" HorizontalAlignment="Left" Margin="103,23,0,0" VerticalAlignment="Top" Width="220" BorderBrush="Black" BorderThickness="1" Height="32" Padding="0"/>

    </Grid>
</Window>

And The .cs Part

using System.ComponentModel;
using System.Windows;

namespace SimpleMVVM
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private AnimalViewModel _animal= new AnimalViewModel ();

        public MainWindow()
        {
            InitializeComponent();

            DataContext = _animal;
            button.Click += (sender, e) => _animal.Name = "Taylor" ;
        }
    }

    public class AnimalViewModel : AnimalModel 
    {
        public AnimalViewModel ()
        {
        }        
    }

    public class AnimalModel : INotifyPropertyChanged
    {
        private string _name;

        public event PropertyChangedEventHandler PropertyChanged;

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged("Name");
                }
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChangedEventArgs args = new PropertyChangedEventArgs(propertyName);
                PropertyChanged(this, args);
            }
        }
    }

}

No imagine that the button click is the update triggered by scheduler, you model get updated first that trigger a property changed event to update the view.

Using this pattern your code will be much reliable.

I hope this helps.

Regards Jegan

like image 145
Jegan Avatar answered Nov 07 '22 12:11

Jegan