【问题标题】:WPF, PRISM and EventAggregratorWPF、PRISM 和 EventAggregator
【发布时间】:2017-01-20 12:19:09
【问题描述】:

在我的应用程序中使用 EventAggregator 时遇到了一些问题。我面临的问题是,在当前处理停止之前,UI 不会更新。我的印象是 EventAggregator 在自己的线程中运行,因此应该能够在事件发布后立即更新 UI。我误解了这个概念吗?

下面是我的代码

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

公共类 LogUpdate : PubSubEvent { }

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

LogEvent 结构

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

然后,如果我将电子表格拖放到 ProcessInputView 上,则会在我的 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"));
        }
    }

除了 ProcessLog 列表视图在 FileDropped 方法完成之前不会更新之外,这一切都很好。通过在 LogEvents.UpdateProcessLogUI 方法之后将 thread.sleep 添加到 FileDropped 方法中可以更清楚地看到这一点。

我是否错误地实现了这一点?如果是,我如何在使用 IEventAggregator 时在 ProcessLogView 列表视图中获得实时更新?

【问题讨论】:

    标签: c# wpf prism eventaggregator


    【解决方案1】:

    我面临的问题是,在当前处理停止之前,UI 不会更新。

    如果您在 ui 线程上执行处理,这是预期的行为。我会将FileDropped 的正文发送到另一个线程(Task.Run)。然后,随着数据处理的进行,它又可以发布进度事件。由于这些是从另一个线程触发的,因此您很可能希望使用 ThreadOption.UIThread 订阅它们。

    我的印象是 EventAggregator 在自己的线程中运行,因此应该能够在事件发布后立即更新 UI。

    EventAggregator 在后台不做任何工作。无论何时调用它,它都会创建一个新订阅或发布一个事件。在所有其他时间它什么都不做,类似于你代码中的所有其他方法......即使它做了,它也不会帮助你,因为你的 ui 线程正忙于运行 FileDropped 并且不会做任何事情否则,直到它完成为止。

    我误解了这个概念吗?

    不过,EventAggregator 可以做什么,这就是后台线程发挥作用的地方,它可以在事件发布时为事件的订阅者生成一个新线程 (ThreadOption.BackgroundThread)。或者它可以将订阅代码编组到 ui 线程 (ThreadOption.UIThread)。

    编辑:重要的旁注:ThreadOption.UIThread 实际上是指ThreadOption.TheThreadTheEventAggregatorWasCreatedOn,所以如果你想用它来将事件编组到 ui 线程,请确保不要在另一个线程上创建 EventAggregator。幸运的是,它是在 ui 线程上正常创建的,但是如果你在后台初始化模块,它可能会发生在后台线程上创建...

    【讨论】:

      【解决方案2】:

      好吧,事实证明我很愚蠢。我的 ProcessInputViewModel 中的 FilesDropped 方法在 UI 线程上运行,因此 UI 当然直到处理完成后才会更新。

      我通过创建一个新方法 FileDroppedBackground 并在新线程上运行它来解决这个问题。

      FileDropped 方法

          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 方法

          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;
          }
      

      这会导致我的 ProcessLogViewModel 中的 UpdateProgressLog 方法出现异常,即无法从另一个线程更新 ObservableCollection

      所以我更新了这个方法如下

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

      我在班级顶部将调度程序定义为Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

      现在,当我运行应用程序并将电子表格拖放到 ProcessInputView 时,日志会实时更新,而不是在方法完成处理时更新

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-14
        相关资源
        最近更新 更多