2017-01-20 3 views
2

Ich habe Probleme mit der Verwendung von EventAggregator in meiner Anwendung. Das Problem, mit dem ich konfrontiert bin, ist, dass die Benutzeroberfläche erst aktualisiert wird, wenn die aktuelle Verarbeitung gestoppt wurde. Ich hatte den Eindruck, dass EventAggregator in einem eigenen Thread lief und daher in der Lage sein sollte, die Benutzeroberfläche zu aktualisieren, sobald ein Ereignis veröffentlicht wurde. Habe ich dieses Konzept missverstanden?WPF, PRISM und EventAggregor

unten ist mein 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; } 
} 

Dann, wenn ich per Drag & Drop eine Tabelle auf den ProcessInputView der folgende Code in meinem ProcessInputViewModel.cs getroffen wird

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

Dies funktioniert alles gut, außer die ProcessList-Listview wird nicht aktualisiert, bis die FileDropped-Methode abgeschlossen ist. Dies wird durch das Hinzufügen einer thread.sleep in die FileDropped-Methode unmittelbar nach der LogEvents.UpdateProcessLogUI-Methode deutlicher.

Habe ich das falsch implementiert und wenn ja, wie bekomme ich Echtzeit-Updates in der ProcessLogView-Listenansicht während der Verwendung von IEventAggregator?

Antwort

1

OK, so stellt sich heraus, ich war ziemlich dumm. Die FilesDropped-Methode in meinem ProcessInputViewModel wurde im UI-Thread ausgeführt, sodass die Benutzeroberfläche erst nach Abschluss der Verarbeitung aktualisiert wurde.

Ich löste dies durch Erstellen einer neuen Methode FileDroppedBackground und das Ausführen in einem neuen Thread.

FileDropped Methode

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 Methode

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

Dies verursacht dann eine Ausnahme innerhalb der UpdateProgressLog Methode in meinem ProcessLogViewModel über die ObservableCollection nicht von einem anderen Thread aktualisiert werden in der Lage

Also habe ich diese Methode wie folgt aktualisiert

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

I definiert Dispatcher als Dispatcher dispatcher = Dispatcher.CurrentDispatcher; an der Spitze meiner Klasse.

Nun, wenn ich die Anwendung ausführen und eine Tabelle falle auf das ProcessInputView das Protokoll in Echtzeit aktualisiert wird, und nicht, wenn die Methode der Verarbeitung beendet

2

Die Frage, die ich bin vor, dass die UI wird nicht Aktualisierung, bis die aktuelle Verarbeitung gestoppt wurde.

Das ist erwartet Verhalten, wenn Sie die Verarbeitung auf dem Ui-Thread durchführen. Ich würde den Körper von FileDropped zu einem anderen Thread senden (Task.Run). Diese wiederum kann die Fortschrittsereignisse im Verlauf der Verarbeitung Ihrer Daten veröffentlichen. Und da diese von einem anderen Thread abgefeuert werden, möchten Sie sie wahrscheinlich mit ThreadOption.UIThread abonnieren.

Ich hatte den Eindruck, dass EventAggregator in einem eigenen Thread lief und daher in der Lage sein sollte, die Benutzeroberfläche zu aktualisieren, sobald ein Ereignis veröffentlicht wird.

Die EventAggregator macht keine Arbeit im Hintergrund. Wenn Sie es aufrufen, erstellt es entweder ein neues Abonnement oder es veröffentlicht ein Ereignis. Zu allen anderen Zeiten tut es einfach nichts, ähnlich wie alle anderen Methoden in deinem Code ... und selbst wenn, würde es dir nicht helfen, weil dein ui-Thread mit FileDropped beschäftigt ist und bis dahin nichts anderes tun wird damit ist es getan.

Habe ich dieses Konzept missverstanden?

Was die EventAggregator tun können, aber, und das ist, wo der Hintergrund-Thread ins Spiel kommt, ist, dass es einen neuen Thread für die Teilnehmer eines Ereignis laichen kann, wenn das Ereignis veröffentlicht (ThreadOption.BackgroundThread). Oder er kann den Subskriptionscode für den ui-Thread bereitstellen (ThreadOption.UIThread).

EDIT: wichtige Randbemerkung: ThreadOption.UIThread bedeutet eigentlich ThreadOption.TheThreadTheEventAggregatorWasCreatedOn, also wenn Sie es zu marshall Ereignisse an den UI-Thread verwenden möchten, achten Sie darauf, nicht die EventAggregator auf einem anderen Thread zu erstellen. Zum Glück wird es normalerweise auf dem ui-Thread erstellt, aber wenn Sie Module im Hintergrund initialisieren, kann es passieren, dass es auf einem Hintergrund-Thread erstellt wird ...