Updating progress log message from Model to ViewModel in MVVM - wpf

I have a method defined in the Model that would execute a long running script where I want to capture the output message when the script is in progress and output to the View via the ViewModel. I understand in order to get realtime update of the output message I should run the Model method in a backgroundworker and raise its ReportProgress event when it has output message to report in order to run the UI update and the script on two separate threads. The problem I have is the backgroundworker object is defined in the ViewModel, so using it to call the Model method is straight forward, but how do I raise the ReportProgress event from the Model method? The only way I can think of is passing in the backgroundworker as input parameter into the method but I feel uneasy about this. Can anyone tell me if this is the right approach in implementing the MVVM framework?
Here are my code stripped to the most bare bone. In my View xaml I have a TextBox bind to the Logger property and DeployCommand command in my ViewModel:
<TextBox Grid.Row="1 " Name="txtOutput" MinHeight="40"
Text="{Binding Logger}"
IsReadOnly="True" Margin="10,10" VerticalScrollBarVisibility="Auto"
IsEnabled="True" MaxLines="2000" TextWrapping="WrapWithOverflow"/>
<Button x:Name="BtnDeploy"
Command="{Binding DeployCommand}"
Content="Deploy"
Height="23"
Margin="5,2"
HorizontalAlignment="Right"
Width="125"
FontFamily="Kalinga"
AutomationProperties.AutomationId="DeployButton"/>
In my ViewModel, the DeployCommand command will trigger the method OnDeploy which in turn will call the Deploy method in Model using the backgroundworker object:
private string logger = string.Empty;
public string Logger
{
get { return logger; }
set
{
logger = value;
RaisePropertyChanged("Logger");
}
}
public ICommand DeployCommand { get; private set; }
public MainWindowViewModel()
{
_worker = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
_worker.DoWork += worker_DoWork;
// _worker.RunWorkerCompleted += worker_RunWorkerCompleted;
_worker.ProgressChanged += worker_ProgressChanged;
DeployController = new DeploymentModel();
this.DeployCommand = new DelegateCommand<object>(this.OnDeploy);
}
private void OnDeploy(object obj)
{
Logger += #"Offline Deployment Started" + System.Environment.NewLine;
if (!_worker.IsBusy)
{
_worker.RunWorkerAsync(DeployController);
}
}
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var deployModel = (DeploymentModel)e.Argument;
deployModel.Deploy(script);
}
private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Logger += e.UserState.ToString();
}
Finally in the Model:
public bool Deploy(string ScriptFile)
{
bool Success = true;
string strCmdText = string.Format(#"/c ""{0}""", ScriptFile);
try
{
var startInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = kitFolder,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
FileName = "cmd.exe",
CreateNoWindow = true,
Arguments = strCmdText,
};
// Launch shell command to run powersheel script
using (Process myProcess = Process.Start(startInfo))
{
// capturing script output message
myProcess.OutputDataReceived += (s, e) =>
{
LogMessage("ExecuteDeploymentKit: " + e.Data);
};
myProcess.ErrorDataReceived += (s, e) =>
{
Success = false;
LogMessage("ExecuteDeploymentKit: ! > " + e.Data);
};
myProcess.BeginErrorReadLine();
myProcess.BeginOutputReadLine();
System.Threading.Thread.Sleep(5000);
myProcess.WaitForExit();
}
}
catch (Exception ex)
{
LogMessage("ExecuteDeploymentKit: " + ex.Message);
return false;
}
if (Success)
{
LogMessage("ExecuteDeploymentKit: Offline Deployment Kit executed successfully");
}
else
{
LogMessage("ExecuteDeploymentKit: Offline Deployment Kit failed");
}
return Success;
}
I have added workder_ProgressChanged to handle the ProgressChanged event of the backgroundworker in order to update the View in the UI thread but without the backgroundworker object in my Model, I can't raise the ProgressChanged event from the method Deploy()
Thanks

The standard way would be for your VM to implement the IProgress interface and pass your M the VM cast as an IProgress object. You shouldn't pass it the VM since that could be a reference nightmare.
But really, the background worker should be implemented in the VM, not the M. And you shouldn't use BackgroundWorker anymore and move onto the new async methods.

If I understand your question right, you might be breaking core principles of MVVM by letting the Model drive your viewmodel and view. Without really having much to go off of, I would suspect that the best approach to this would be to actually create a "service".
Keep your model dumb and let it only contain data. Think POCO. Then, utilize a service that implements a background worker. Have your View Model run the service. The View model can call the service and provide that service a reference to your instantiated model. This way, you aren't heavily coupling your model to your view model.

Related

Datagrid remains empty after asynchronous initialization in view model constructor

I have a WPF application with a view containing a data grid and a view model with an observable collection that is initialized by calling an asynchronous method in the constructor. But the data grid remains empty upon running the code.
The view model class looks like this.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
Task.Run(() => LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I all LoadBookingRecords() in the constructor, so that the data starts loading on initialization of the view model already but I do it asynchronously, so it does not block the UI thread and makes the application unresponsive.
I have tried waiting for the completion of the task in the constructor via
Task.Run(() => LoadBookingRecords()).Wait();
to check that this has something to do with the asynchronous function call. And indeed, if I wait for the method to finish in the constructor, the data grid displays correctly. But I don't want to wait for the task to finish on the UI thread because it blocks the UI.
I have read that you must raise the PropertyChanged event on the UI thread to trigger a UI update and I suppose that is the problem here. I have also read that one can use
Application.Current.Dispatcher.BeginInvoke()
to schedule a delegate to run on the UI thread as soon as possible, so I tried the following.
private async Task LoadBookingRecords()
{
await Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}));
}
But this leaves the DataGrid empty as well.
"'asynchronous ... in constructor" is something you must avoid.
Async calls must be awaited, which can not be done in a constructor. Declare an awaitable Initialize method instead
public Task Initialize()
{
return LoadBookingRecords();
}
and call it in an async Loaded event handler in your MainWindow:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await viewModel.Initialize();
}
Alternatively, create a factory method like
public static async Task<MainWindowViewModel> Create()
{
var viewModel = new MainWindowViewModel();
await viewModel.LoadBookingRecords();
return viewModel;
}
and call that in the Loaded handler:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
DataContext = await MainWindowViewModel.Create();
}
Building on Clemens' answer, I tried something a little different in order to avoid touching the MainWindow code-behind.
I removed the call on LoadBookingRecords in the constructor and instead created a delegate command as a property that holds this method.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ICommand LoadBookingRecordsCommand { get; set; }
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
LoadBookingRecordsCommand = new DelegateCommand(async _ => await LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I then added the NuGet package Microsoft.Xaml.Behaviors.Wpf to the project and added the following namespace to the MainWindow XAML.
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Finally, I bound the delegate command to the MainWindow's Loaded event.
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadBookingRecordsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Now the data grid displays correctly after being loaded.

InvokeCommand from Observable.Timer causes cross-thread issue

I have a ReactiveCommand that refreshes data and is bound to a Button in XAML. The functionality works fine, but I also want to execute the command on a timer.
I have the following code - SetupAutoRefresh is called from the ctor in my VM, but when the Observable fires, I get an exception with the message: "The calling thread cannot access this object because a different thread owns it."
VM:
private void SetupAutoRefresh() {
Observable.Timer(TimeSpan.FromSeconds(5))
.Select(_ => Unit.Default)
.ObserveOn(RxApp.MainThreadScheduler)
.InvokeCommand(RefreshData);
RefreshData = ReactiveCommand.CreateFromTask(Refresh);
}
private async Task Refresh()
{
var updatedData = await _repository.GetAll();
Data.Merge(updatedData);
}
private ReactiveCommand<Unit, Unit> _refreshData;
public ReactiveCommand<Unit, Unit> RefreshData
{
get { return _refreshData; }
set { this.RaiseAndSetIfChanged(ref _refreshData, value); }
}
private IReactiveList<Model> _data;
public IReactiveList<Model> Data
{
get { return _data; }
set { this.RaiseAndSetIfChanged(ref _data, value); }
}
XAML:
<Button Grid.Column="2"
Command="{Binding RefreshData}"
Style="{StaticResource ToolbarButtonTheme}"
Content="{StaticResource RefreshToolbarIcon}"
ToolTip="Refresh Search"/>
Debug output provides this stacktrace:
at System.Windows.Threading.Dispatcher.VerifyAccess()
at System.Windows.DependencyObject.GetValue(DependencyProperty dp)
at System.Windows.Controls.Primitives.ButtonBase.get_Command()
at System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute()
at System.Windows.Controls.Primitives.ButtonBase.OnCanExecuteChanged(Object >sender, EventArgs e)
at
System.Windows.Input.CanExecuteChangedEventManager.HandlerSink.OnCanExecuteChanged(Object sender, EventArgs e)
at ReactiveUI.ReactiveCommand.OnCanExecuteChanged() in C:\projects\reactiveui\src\ReactiveUI\ReactiveCommand.cs:line 628
I've tried many different variations of attempting to schedule this on the RxApp.MainThreadScheduler but without any joy - ObserveOn, SubscribeOn, setting the output scheduler... none of which I had much hope for anyway.
Feel like I'm missing something obvious here, but have been banging my head against a brick wall for the whole afternoon. Surely this scenario is possible in RxUI?
The Refresh method runs on a background thread; you can't modify databound properties within that method.
Try this:
private void SetupAutoRefresh() {
Observable.Timer(TimeSpan.FromSeconds(5))
.Select(_ => Unit.Default)
// remove ObserveOn here; the Command will run on the background
.InvokeCommand(RefreshData);
RefreshData = ReactiveCommand.CreateFromTask(Refresh);
// RefreshData.Subscribe is guaranteed to run on the UI thread
RefreshData.Subscribe(listOfModels => Data.Merge(listOfModels))
}
private async Task Refresh()
{
// all this method does is deliver a list of models
return await _repository.GetAll();
}
// return IEnumerable<Model> from the command
public ReactiveCommand<Unit, IEnumerable<Model>> RefreshData
Now, your ReactiveCommand simply fetches the new data, and returns it to you on the UI thread within Subscribe :)
Figured out the issue - looks like the Observable needed to be created on the UI thread. I missed it from the original post, but the SetupAutoRefresh method had been called from another async method, which had switched context during a prior await.

Prism, IConfirmNavigationRequest, InteractionRequest and Async

I have an issue, i coded my view, viewmodel creation into the ModuleInit.Initialize method
this.container.RegisterType<IControlPanel, ViewModels.SeveritiesViewModel>("SeveritiesViewModel");
this.container.RegisterType<object, Views.SeveritiesView>("SeveritiesView", new InjectionConstructor(new ResolvedParameter<IControlPanel>("SeveritiesViewModel")));
SeveritiesVeiwModel inherits from ViewModelBase
public class ViewModelBase : BindableBase, IControlPanel, INavigationAware, IConfirmNavigationRequest
Constructor for ViewModelBase calls two virtual methods. Initialize and GetData.
GetData performs some data access methods using async await.
so the problem i have is Prism constructs my SeveritiesViewModel, the GetData method runs, and throws and exception which i catch. i would then like to display a dialog using the InteractionRequest, however the view.DataContext has not yet be set, hence no bindings or Interaction.Triggers to receive the InteractionRequest.
so i thought i should look into RegionManager.RequestNaviagte using a callback. i thought since all my viewmodels implement IConfirmNavigationRequest i could return false in the NavigationResult from the View/viewmodel being injected. however ConfirmNavigationRequest is never called. this is wpf not silverlight?
so how do i work this extremely decoupled application. do i need to implement some type of shared service?
I guess i am going to need to store exceptions until the view has finished binding with the viewmodel, perhaps implement my own interface with a method to check an exceptions collection and in the view call the interface method?
why is ConfirmNavigationRequest never called?
InteractionRequest work great after the DataContext is set, but before; i'm at a loss.
Any advise would be appreciated.
Thanks
Gary
here is some code.
toolbar button command click runs the following.
this.regionManager.RequestNavigate("ContentRegion", "SeveritiesView");
here is the code behind for the view.
public partial class SeveritiesView : UserControl, IApplicationView
{
public SeveritiesView(IControlPanel model)
{
InitializeComponent();
this.DataContext = model;
}
public string ViewName
{
get { return "SeveritiesView"; }
}
}
ViewModelBase.
protected ViewModelBase(bool initializeDB = true)
{
notifications = new List<NotificationWindowNotification>();
this.uiFactory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
NotificationRequest = new InteractionRequest<NotificationWindowNotification>();
ConfirmationRequest = new InteractionRequest<ConfirmationWindowNotification>();
if (initializeDB)
{
EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder(System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
entityBuilder.ProviderConnectionString = EventLogAnalysis.Properties.Settings.Default.ConnectionString;
db = new ServerEventLogEntities(entityBuilder.ToString());
}
ThrobberVisible = Visibility.Visible;
Initialize();
GetData();
}
SeveritiesViewModel.
public SeveritiesViewModel(IRegionManager regionManager, IEventAggregator eventAggregator) : base()
{
try
{
this.regionManager = regionManager;
this.eventAggregator = eventAggregator;
eventAggregator.GetEvent<AddSeverity>().Subscribe(AddSeverity);
eventAggregator.GetEvent<DeleteSeverity>().Subscribe(DeleteSeverity);
}
catch(Exception e)
{
uiFactory.StartNew(() =>
NotificationRequest.Raise(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
})
).Wait();
}
}
protected async override void GetData()
{
try
{
List<Task> tasks = new List<Task>();
tasks.Add(GetEventFilterSeverities());
await Task.WhenAll(tasks).ContinueWith((t) =>
{
ThrobberVisible = Visibility.Collapsed;
eventAggregator.GetEvent<RecordStatusEvent>().Publish(new RecordStatusMessage() { CanAdd = true, CanDelete =(currentEventFilterSeverity != null), IsClosing = false });
}
, TaskScheduler.FromCurrentSynchronizationContext());
}
catch(Exception e)
{
notifications.Add(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
});
}
}
protected async Task GetEventFilterSeverities()
{
try
{
throw new NullReferenceException("My exception");
ObservableCollection<EventFilterSeverity> _eventFilterSeverities = new ObservableCollection<EventFilterSeverity>();
var eventFilterSeverities = await (from sg in db.EventFilterSeverities
orderby sg.EventFilterSeverityID
select sg).ToListAsync();
foreach (EventFilterSeverity efs in eventFilterSeverities)
_eventFilterSeverities.Add(efs);
EventFilterSeverities = _eventFilterSeverities;
}
catch(Exception e)
{
notifications.Add(new NotificationWindowNotification()
{
Title = string.Format("Error during {0}.{1}"
, ModuleName, System.Reflection.MethodBase.GetCurrentMethod().Name),
Content = string.Format("{0}", e.Message)
});
}
}
Two fairly easy solutions;
Do not start data access until the Shell has been displayed and interactions are possible
Catch the exception and immediatly await on a Task that completes when the interaction request becomes available. Is this when the navigation completes? This effectively queues the interaction for when it can be displayed.
This looks promising.
in the view
<i:Interaction.Triggers>
<i:EventTrigger EventName="Raised" SourceObject="{Binding NotificationRequest}">
<i:EventTrigger.Actions>
<dialogs:PopupWindowAction IsModal="True"/>
</i:EventTrigger.Actions>
</i:EventTrigger>
<i:EventTrigger EventName="Loaded">
<ei:CallMethodAction TargetObject="{Binding Mode=OneWay}" MethodName="DisplayPreBoundExceptions"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In the ViewModelBase
public void DisplayPreBoundExceptions()
{
notifications.ForEach((t) => NotificationRequest.Raise(t));
notifications.Clear();
}

Handling WPF button double-click in MVVM pattern

I have a button in an MVVM application that is hooked up to a command in the view model. The handler for the view model command does some file I/O (particularly calling File.Copy to create or overwrite an existing file).
Now, it seems that when I double-click on the button, this command handler gets called twice. Since both handlers are now trying to access the same file to copy/overwrite it at the same time, I'm getting an IOException.
Is there anyway to deal with this situation short of catching the IOException and ignoring it? This does not seem to be a guaranteed catch although there may be unrelated problems with the rest of the system that causes that to happen.
Use a value in the the ViewModel to protect the code that would be running when a click occurs. Set a value like: bool bFileIO = false;
Then in your handler function:
if (!bFileIO)
{
bFileIO = true;
//file IO here
bFileIO = false;
}
Something like that would protect the multi-clicking from trying to run multiple times.
The easiest way of doing this is to have your command return false in CanExecute while you're executing. This will prevent the second click from happening (as your button will be disabled). If using a DelegateCommand from prism:
private readonly DelegateCommand<object> copyCommand;
private bool isCopying = false;
public MyViewModel()
{
copyCommand = new DelegateCommand<object>(
_ => !isCopying,
_ =>
{
if (isCopying) return; // this shouldn't be required, but here for safety
isCopying = true;
copyCommand.RaiseCanExecuteChanged();
// do copy
isCopying = false;
copyCommand.RaiseCanExecuteChanged();
});
}
I think you should use the CanExecute of your command to control your button.
<Button Command="{Binding WriteFileCommand}" Content="Button" Height="23" HorizontalAlignment="Left" Margin="273,194,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
and the viewmodel
public class MyViewModel
{
private bool isWritingFile = false;
public DelegateCommand WriteFileCommand
{
get;
private set;
}
public bool IsWritingFile
{
get
{
return isWritingFile;
}
set
{
isWritingFile = value;
WriteFileCommand.RaiseCanExecuteChanged();
}
}
public MyViewModel()
{
WriteFileCommand = new DelegateCommand(WriteFile, CanWriteFile);
}
private void WriteFile()
{
IsWritingFile = true;
// write the file
// FileStream stream = new FileStrem(...)
//
IsWritingFile = false;
}
}

Async code in .NET 4.0

I have the following code running in a WPF app:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
object obj = new object();
Collection.Add(obj);
Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(delegate(object sender2, NotifyCollectionChangedEventArgs e2)
{
if (Collection.Count == 0)
App.Current.MainWindow.Close();
});
Task.Factory.StartNew(() =>
{
//Do long running process
Collection.Remove(obj); //this errors out
});
}
private ObservableCollection<object> Collection = new ObservableCollection<object>();
}
I get the error System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
I was under the impression that Task.Factory.StartNew queued up an async task, so the thread should be the same, no?
Task.Factory.StartNew executes your action in the default TaskScheduler, so it will run in the ThreadPool.
ObservableCollection is not thread-safe. It means that your CollectionChanged handler, which performs operations on UI controls ( App.Current.MainWindow.Close() ) is not going to be executed in the UI thread because the collection modification is being done in your Task's action, causing the error you are seeing.
If you only need to interact with the UI in your handler, you can use the dispatcher:
Collection.CollectionChanged += new NotifyCollectionChangedEventHandler(delegate(object sender2, NotifyCollectionChangedEventArgs e2)
{
if (Collection.Count == 0)
this.Dispatcher.BeginInvoke((Action)(()=> App.Current.MainWindow.Close()));
});
If you need to bind to it, consider using a thread-safe implementation. See this.
Just to add to Arthur's answer, in my real application (not the sample code above) I needed to do this from an MvvmLight view model. To access the dispatcher from a ViewModel:
Inside App, add the following:
static App()
{
DispatcherHelper.Initialize();
}
And then instead of calling this.Dispatcher, because a ViewModel has no reference to the Dispatcher, the following will work:
DispatcherHelper.UIDispatcher.BeginInvoke((Action)(() => App.Current.MainWindow.Close()));

Resources