gheff
gheff

Reputation: 193

WPF, PRISM and EventAggregrator

I'm having some trouble with using EventAggregator within my application. The issue I am facing is that the UI will not update until the current processing has stopped. I was under the impression that EventAggregator ran in its own thread and therefore should be able to update the UI as soon as an event is published. Have I misunderstood this concept?

below is my code

Bootstrapper.cs

class Bootstraper : UnityBootstrapper
{
    protected override DependencyObject CreateShell()
    {
        return ServiceLocator.Current.GetInstance<MainWindow>();
    }

    protected override void InitializeShell()
    {
        Application.Current.MainWindow.Show();
    }
}

App.xmal.cs

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var bs = new Bootstraper();
        bs.Run();
    }
}

MainWindow.xmal

<Window x:Class="TransactionAutomationTool.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TransactionAutomationTool"
    xmlns:views="clr-namespace:TransactionAutomationTool.Views"
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True"
    mc:Ignorable="d"
    Title="MainWindow" Height="600" Width="800">
<Grid>
    <views:HeaderView x:Name="HeaderViewCntl" Margin="20,21,10,0" Height="70" Width="740" HorizontalAlignment="Left" VerticalAlignment="Top" />
    <views:ProcessSelectionView x:Name="ProcessSelectionViewControl" Margin="20,105,0,0" Height="144" Width="257" HorizontalAlignment="Left" VerticalAlignment="Top" />
    <views:ProcessInputView x:Name="ProcessInputViewControl" Margin="20,280,0,0" Height="218" Width="257" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    <views:ProcessLogView x:Name="ProcessLogViewControl" Margin="298,105,0,0" Height="445" Width="462" HorizontalAlignment="Left" VerticalAlignment="Top" />
    <views:ButtonsView x:Name="ButtonViewControl" Margin="0,513,0,0" Height="37" Width="300" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>

ProcessLogView.xaml

<UserControl x:Class="TransactionAutomationTool.Views.ProcessLogView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:TransactionAutomationTool.Views"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
         xmlns:prism="http://prismlibrary.com/"
         prism:ViewModelLocator.AutoWireViewModel="True"
         mc:Ignorable="d" 
         d:DesignHeight="445" d:DesignWidth="462">
<UserControl.Resources>
    <DataTemplate x:Key="TwoLinkMessage">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Message}" />
                <TextBlock>
                    <Hyperlink NavigateUri="{Binding Link}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="HyperLinkClicked">
                                <ei:CallMethodAction MethodName="HyperLinkClicked" TargetObject="{Binding}" />
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                        <TextBlock Text="{Binding Link}"/>
                    </Hyperlink>
                </TextBlock>
            <TextBlock>
                <Hyperlink NavigateUri="{Binding SecondLink}">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="HyperLinkClicked">
                            <ei:CallMethodAction MethodName="HyperLinkClicked" TargetObject="{Binding}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                    <TextBlock Text="{Binding SecondLink}"/>
                </Hyperlink>
            </TextBlock>
        </StackPanel>
    </DataTemplate>
    <DataTemplate x:Key="LinkMessage">
        <TextBlock>
            <Hyperlink NavigateUri="{Binding Link}">
                <i:Interaction.Triggers>
                        <i:EventTrigger EventName="HyperLinkClicked">
                            <ei:CallMethodAction MethodName="HyperLinkClicked" TargetObject="{Binding}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                <TextBlock Text="{Binding Message}"/>
            </Hyperlink>
        </TextBlock>
    </DataTemplate>
    <DataTemplate x:Key="Default">
        <TextBlock Text="{Binding Message}" />
    </DataTemplate>
</UserControl.Resources>
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="15">
    <!--<ListBox x:Name="lbxProgress" HorizontalAlignment="Left" Height="408" Margin="5,5,0,0" VerticalAlignment="Top" Width="431" Foreground="Black" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding LogMessage}" BorderThickness="0" />-->
    <ListView Name="lvProgress" ItemsSource="{Binding LogMessage}" Margin="9" BorderThickness="0">
        <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="ContentTemplate" Value="{StaticResource Default}" />
                <Style.Triggers>
                    <DataTrigger Binding="{Binding LinkNum}" Value="0">
                        <Setter Property="ContentTemplate" Value="{StaticResource Default}" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding LinkNum}" Value="1">
                        <Setter Property="ContentTemplate" Value="{StaticResource LinkMessage}" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding LinkNum}" Value="2">
                        <Setter Property="ContentTemplate" Value="{StaticResource TwoLinkMessage}" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ListView.ItemContainerStyle>
    </ListView>
</Border>

ProcessLogViewModel.cs

class ProcessLogViewModel: EventsBase
{

    private ObservableCollection<LogPayload> logMessage;

    public ObservableCollection<LogPayload> LogMessage
    {
        get { return logMessage; }
        set { SetProperty(ref logMessage, value); }
    }

    public ProcessLogViewModel()
    {
        //If statement is required for viewing the MainWindow in design mode otherwise errors are thrown
        //as the ProcessLogViewModel has parameters which only resolve at runtime. I.E. events
        if (!(bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue)
        {
            events.GetEvent<LogUpdate>().Subscribe(UpdateProgressLog);
            LogMessage = new ObservableCollection<LogPayload>();
        }
    }

    public void HyperLinkClicked(object sender, RequestNavigateEventArgs e)
    {
        System.Diagnostics.Process.Start(e.Uri.AbsoluteUri);
    }

    private void UpdateProgressLog(LogPayload msg)
    {
        LogMessage.Add(msg);
    }
}

EventsBase.cs

public class EventsBase: BindableBase
{
    public static IServiceLocator svc = ServiceLocator.Current;
    public static IEventAggregator events = svc.GetInstance<IEventAggregator>();
}

LogEvents.cs

public class LogUpdate : PubSubEvent { }

public class LogEvents : EventsBase
{
    public static void UpdateProcessLogUI(LogPayload msg)
    {
        events.GetEvent<LogUpdate>().Publish(msg);
    }
}

LogEvent struct

public struct LogPayload
{
    public string Message { get; set; }
    public int LinkNum { get; set; }
    public string Link { get; set; }
    public string SecondLink { get; set; }
}

Then if I drag and drop a spreadsheet on to the ProcessInputView the following code is hit within my ProcessInputViewModel.cs

    public void FileDropped(object sender, DragEventArgs e)
    {
        string[] files;
        string[] cols;
        TextBox txtFileName = (TextBox)sender;
        SpreadsheetCheck result = new SpreadsheetCheck();
        DDQEnums.TranTypes tranType;
        List<string> fileFormats = new List<string>();

        fileFormats.Add(Constants.FileFormats.XLS);
        fileFormats.Add(Constants.FileFormats.XLSX);

        if (e.Data.GetDataPresent(DataFormats.FileDrop, true))
        {
            files = e.Data.GetData(DataFormats.FileDrop, true) as string[];

            if (files.GetLength(0) > 1)
            {
                result.IsValid = false;
                result.Message = "Only drop one file per input box";
            }
            else
            {
                result = Utils.CheckIfSpreadsheetIsValidForInput(files[0], fileFormats, (DDQEnums.TranTypes)txtFileName.Tag, out tranType);

                LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload(string.Format("Checking {0} Spreadsheet Column Format", tranType)));
                if (result.IsValid)
                {
                    cols = Utils.GetSpreadsheetColumns(tranType);
                    if (cols.GetLength(0) > 0)
                    {
                        result = CheckSpreadsheetColumnFormat(files[0], cols, tranType);
                        txtFileName.Text = Path.GetFileName(files[0]);
                    }
                    else
                    {
                        result.IsValid = false;
                        result.Message = "Unable to get column definations to be used";
                    }
                }
            }
            IsInputValid = result.IsValid;
            LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload(result.Message));
            ProcessInputViewEventsPublish.SendInputValidStatus(IsInputValid, SelectedProcess, files[0]);
        }
        else
        {
            LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload("Unable to get the file path for the dropped file"));
        }
    }

This all works fine except the ProcessLog listview is not updated until the FileDropped method has completed. This can been seen clearer by adding a thread.sleep into the FileDropped method just after the LogEvents.UpdateProcessLogUI method.

Have I implemented this incorrectly and if so how do I get real time updates in the ProcessLogView listview while using IEventAggregator?

Upvotes: 2

Views: 755

Answers (2)

Haukinger
Haukinger

Reputation: 10863

The issue I am facing is that the UI will not update until the current processing has stopped.

That's expected behavior if you perform the processing on the ui thread. I'd send the body of FileDropped to another thread (Task.Run). That in turn can then publish the progress events as it progresses with the processing of your data. And because those are fired from another thread, you most likely want to subscribe to them with ThreadOption.UIThread.

I was under the impression that EventAggregator ran in its own thread and therefore should be able to update the UI as soon as an event is published.

The EventAggregator doesn't do any work in the background. Whenever you call it, it either creates a new subscription or it publishes an event. At all other times it just does nothing, similar to all the other methods in your code... and even if it did, it wouldn't help you, because your ui thread is busy running FileDropped and won't do anything else until it's done with that.

Have I misunderstood this concept?

What the EventAggregator can do, though, and that's where the background thread comes into play, is that it can spawn a new thread for the subscriber of an event when the event is published (ThreadOption.BackgroundThread). Or it can marshall the subscribing code to the ui thread (ThreadOption.UIThread).

EDIT: important side note: ThreadOption.UIThread actually means ThreadOption.TheThreadTheEventAggregatorWasCreatedOn, so if you want to use it to marshall events to the ui thread, be sure not to create the EventAggregator on another thread. Luckily, it's created on the ui thread normally, but if you initialize modules in the background, it can happen that it's created on a background thread...

Upvotes: 2

gheff
gheff

Reputation: 193

OK, so turns out I was being pretty stupid. The FilesDropped method within my ProcessInputViewModel was running on the UI thread so of course the UI didn't update until after processing had finished.

I solved this by creating a new method FileDroppedBackground and running this on a new thread.

FileDropped method

    public void FileDropped(object sender, DragEventArgs e)
    {
        TextBox txtFileName = (TextBox)sender;
        DDQEnums.TranTypes tag = (DDQEnums.TranTypes)txtFileName.Tag;
        string fileName = string.Empty;

        new Thread(() => fileName = FileDroppedBackground(tag, e)).Start();
        txtFileName.Text = fileName;
    }

FileDroppedBackground method

    private string FileDroppedBackground(DDQEnums.TranTypes tag, DragEventArgs e)
    {
        string[] files;
        string[] cols;

        string returnValue = string.Empty;


        SpreadsheetCheck result = new SpreadsheetCheck();
        DDQEnums.TranTypes tranType;
        List<string> fileFormats = new List<string>();

        fileFormats.Add(Constants.FileFormats.XLS);
        fileFormats.Add(Constants.FileFormats.XLSX);

        if (e.Data.GetDataPresent(DataFormats.FileDrop, true))
        {
            files = e.Data.GetData(DataFormats.FileDrop, true) as string[];

            if (files.GetLength(0) > 1)
            {
                result.IsValid = false;
                result.Message = "Only drop one file per input box";
            }
            else
            {
                result = Utils.CheckIfSpreadsheetIsValidForInput(files[0], fileFormats, tag, out tranType);

                LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload(string.Format("Checking {0} Spreadsheet Column Format", tranType)));
                Thread.Sleep(10000);

                if (result.IsValid)
                {
                    cols = Utils.GetSpreadsheetColumns(tranType);
                    if (cols.GetLength(0) > 0)
                    {
                        result = CheckSpreadsheetColumnFormat(files[0], cols, tranType);
                        returnValue = Path.GetFileName(files[0]);
                    }
                    else
                    {
                        result.IsValid = false;
                        result.Message = "Unable to get column definations to be used";
                    }
                }
            }
            IsInputValid = result.IsValid;
            LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload(result.Message));
            ProcessInputViewEventsPublish.SendInputValidStatus(IsInputValid, SelectedProcess, files[0]);
        }
        else
        {
            LogEvents.UpdateProcessLogUI(Utils.BuildLogPayload("Unable to get the file path for the dropped file"));
        }

        return returnValue;
    }

This then caused an exception within the UpdateProgressLog method in my ProcessLogViewModel about the ObservableCollection not being able to be updated from another thread

so I updated this method as follows

    private void UpdateProgressLog(LogPayload msg)
    {
        dispatcher.Invoke(new Action(() => { LogMessage.Add(msg); }));
    }

I defined dispatcher as Dispatcher dispatcher = Dispatcher.CurrentDispatcher; at the top of my class.

Now when I run the application and drop a spreadsheet on to the ProcessInputView the log is updated in real-time and not when the method finishes processing

Upvotes: 1

Related Questions