I have a ProgressBar in my MVVM View, which is bound to a View Model property. Updating the property in the VM all works correctly. However, I have some longer-running file/network operations which take place in another class (Model), and I would like to update the ProgressBar property in the middle of the Model operations. I can't pass the ProgressBar property via reference to the Model class. I definitely don't want to pass a handle to the VM to the Model. How do I update this VM property from the Model classes adData and fileOps?
Edit: Additional code to show where I need to update the ProgressBar property.
View
<ProgressBar Value="{Binding ProgressMeter}"/>
<TextBlock Text="{Binding CurrentStatusMsg}"/>
ViewModel
public class ViewModel : INotifyPropertyChanged
{
private readonly IADData adData;
private readonly IFileOps fileOps;
public ViewModel(IADData adData, IFileOps fileOps)
{
this.adData = adData;
this.fileOps = fileOps;
}
// INPC Implementation goes here
private int progressMeter;
public int ProgressMeter
{
get => progressMeter;
set
{
if (progressMeter != value)
{
progressMeter = value;
RaisePropertyChanged("ProgressMeter");
}
}
}
// Similar property for CurrentStatusMsg
public void DoIt()
{
BackgroundWorker bgWorker = new BackgroundWorker
{
WorkerReportsProgress = true
};
bgWorker.DoWork += CreatePhoneList;
bgWorker.RunWorkerCompleted += BgWorker_RunWorkerCompleted;
CurrentStatusMsg = "Creating Phone List...";
ProgressMeter = 5;
bgWorker.RunWorkerAsync();
}
private void CreatePhoneList(object sender, DoWorkEventArgs e)
{
// How do I update ProgressMeter in adData and fileOps classes?
DataTable t = adData.ReportLines();
fileOps.AddDeptRows(t);
e.Result = t;
}
private void BgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ProgressMeter = 100;
CurrentStatusMsg = "Creating Phone List... Complete.";
reportCreator.ShowReport((DataTable)e.Result);
}
}
Thanks to #Adam Vincent, I ended up using the Messenger Pattern - specifically this implementation. I can now pass the ProgressBar value from the Model to the View, without breaking abstraction layers.
Related
Before I get into the details here, I'm still in what I would consider to be the "learning" phase of my C#/WPF journey... so apologies if what I'm asking here is stupidly obvious...
I have a small application (WPF, .NET Framework 4.8) that does the following:
read a list of values
do something for each value in the list
I am trying to do this with a BackgroundWorker so that I can report back to the UI as the list is being processed, preferably with a progress bar.
For the moment, the DoWork method just has some code in there to indicate that it's actually going through the process as expected and so that I could check that all the UI is updating as expected before I put the actual "what I want it to do" in there.
It seems that all the properties are updating as expected, but the UI (i.e. the progress bar) just doesn't move.
And I have checked that the data context in the XAML is set correctly (both in the XAML and in the Code-Behind).
In my XAML I have the following:
<ProgressBar x:Name="ProgressBar"
Width="740"
Height="20"
Background="Transparent"
Foreground="#008DEB"
Grid.Row="3"
Minimum="0"
Maximum="100"
Value="{Binding ProgressBarIndicator}"/>
And in my class containing all my methods/properties etc, I have:
private int _measurementProgress;
public int MeasurementProgress
{
get { return _measurementProgress; }
set
{
_measurementProgress = value;
OnPropertyChanged();
}
}
private int _progressBarIndicator;
public int ProgressBarIndicator
{
get { return _progressBarIndicator; }
set
{
_progressBarIndicator = value;
OnPropertyChanged();
}
}
public void StartMeasurements(string ipAddress)
{
TotalMeasurementsInList = CommandsList.Count;
MeasurementProgress = 0;
measurementWorker.WorkerReportsProgress = true;
measurementWorker.DoWork += worker_DoWork;
measurementWorker.ProgressChanged += worker_ProgressChanged;
measurementWorker.RunWorkerCompleted += worker_RunWorkerCompleted;
measurementWorker.RunWorkerAsync();
}
public void worker_DoWork(object sender, DoWorkEventArgs e)
{
foreach (var command in CommandsList)
{
MessageBox.Show(String.Format("Measuring Sample: {0}",command.SampleName),"Measuring Sample");
measurementWorker.ReportProgress((int)((double)(MeasurementProgress / (double)TotalMeasurementsInList)*100));
Thread.Sleep(command.DelayTime*1000);
}
}
public void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
MeasurementProgress++;
MessageBox.Show(String.Format("Progress is {0}%", e.ProgressPercentage.ToString()));
ProgressBarIndicator = e.ProgressPercentage;
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ProgressBarIndicator = 100;
MessageBox.Show("Measurements are completed","Finished");
}
The "OnPropertyChanged()" method is inside my ObservableObject class, and the above class is set to inherit from this ObservableObject class. The ObservableObject class looks like this:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyname = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
}
By placing break-points in strategic places, I can confirm that the value of MeasurementProgress does indeed increment as one would expect, as does the value of ProgressBarIndicator. This was double-confirmed by the MessageBox in the worker_ProgressChanged method as it does indeed display the appropriate percentage value.
Annoyingly, what would appear to me to be exactly the same code is working as expected in another part of the application. The code here is more or less a copy/paste from there... but I just can't see what I'm doing wrong.
Any help or pointers where I could look to try and debug this appreciated.
Many thanks
Colin
I am having a little bit of trouble getting a ProgressBar to work. When I start it up, nothing happens and I can't see why?
I thought that this would start the task worker.RunWorkerAsync();
The below example should be able to be copied and pasted into a new solution and be run for testing if needed.
The XAML,
<Grid Margin="20">
<ProgressBar
Height="60"
Minimum="0"
Maximum="100"
Value="{Binding Progress, Mode=OneWay}"
Visibility="{Binding ProgressVisibility}"/>
</Grid>
My code,
public partial class MainWindow : Window
{
public ProggressbarViewModel PsVm { get; set; }
public MainWindow()
{
InitializeComponent();
PsVm = new ProggressbarViewModel();
}
public class ProggressbarViewModel
{
public ProggressbarViewModel()
{
var worker = new BackgroundWorker();
worker.DoWork += DoWork;
worker.ProgressChanged += ProgressChanged;
worker.RunWorkerAsync();
}
private int _progress;
public int Progress
{
get { return _progress; }
set
{
if (_progress == value) return;
_progress = value;
OnPropertyChanged();
}
}
private void DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
_progress = i;
OnPropertyChanged();
}
}
private void ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Progress = e.ProgressPercentage;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Any help would be much appreciated.
EDIT: The question is is similar possibly a duplicate in that sense, however the linked answer did not solve my problem, like it states in the Duplicate banner.
When you're not explicitly indicating source object for your bindings (by means of Binding.Source or Binding.RelativeSource properties), the framework uses (possibly inherited) value of DataContext of the target object as the source. The problem is that you don't assign your view-model to the DataContext property of any control. Thus, the source for the bindings resolves to null and nothing is showing on your progress bar.
To resolve your issue you should assign your view model to the DataContext of your MainWindow:
public MainWindow()
{
InitializeComponent();
PsVm = new ProggressbarViewModel();
DataContext = PsVm;
}
If however you're planning on using different DataContext for your window, you can bind DataContext of ProgressBar:
<ProgressBar
DataContext="{Binding Path=PsVm,
RelativeSource={RelativeSource AncestorType=local:MainWindow}}"
(...) />
You could also modify particular bindings by prepending PsVm. to the value of Path and using RelativeSource, e.g.:
Value="{Binding Path=PsVm.Progress,
RelativeSource={RelativeSource AncestorType=local:MainWindow},
Mode=OneWay}"
In that case however you'd have to modify each binding, so previous solutions are quicker and/or simpler.
As a side note, you might also want to change the way you're reporting progress - note that OnPropertyChanged in your DoWork method is not called from UI thread. The proper way to do it would be:
private void DoWork(object sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
worker.ReportProgress(i); //This will raise BackgroundWorker.ProgressChanged
}
}
Also, in order to support progress reporting, you should set WorkerReportsProgress to true on your worker, e.g.:
var worker = new BackgroundWorker { WorkerReportsProgress = true };
I have a MVVM view and viewmodel. In the constructor of my viewmodel I pass a list of IObservable messages and subscribe to them through a simple class sitting outide of my viewmodel and view
Outside class
{
viewModel =
new ViewModelClass(
responseHandler.AsObservable());
viewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModelClass.MyProperty))
{
// Error here
view = new MyViewClass() { DataContext = viewModel };
}
}
In the view model constructor
subscription = receiveMessages.Subscribe(MessageReceived);
private void MessageReceived(GvsMessage message)
{
MyProperty = true;
}
On receiving a message I want to create my view not before that. Although the viewmodel is created before to handle property change etc
The problem is that I get "the calling thread must be sta because many ui components require this". Could someone please help
As we spoke in the comments you need to use a Dispatcher which can be acquired from different parts of the app.
To initialize the dispatcher you can use this snippet:
protected static Dispatcher _d;
if (Application.Current != null)
{
_d = Application.Current.Dispatcher;
}
else
{
_d = Dispatcher.CurrentDispatcher;
}
Explanation:
The first dispatcher is done when the application has UnitTests when you run the tests this dispatcher will not be null,
The second one is the current Dispatcher used by your application.
After you have this in your code when initializing your VM now you can send messages Actions, Events to the UI Thread.
I have a little method just for that:
public static void UIThread(Action action)
{
if (_d == null)
{
if (Application.Current != null)
{
_d = Application.Current.Dispatcher;
}
else
{
_d = Dispatcher.CurrentDispatcher;
}
}
_d.Invoke(action);
}
This function will accept lambda, like so:
UIThread(() =>
{
Processing = true;
Message = "Working ...";
//in your case you would raise the Loaded event here
});
This way you EventHandler in your view will have no problem showing that view.
If you need any more info let us know.
HTH
I resolved this by creating the view and the viewmodel in the constructor. In the propertychanged event I just set a property IsVisible to 'true' binds the window visibility
<Window.Visibility>
<Binding Path="IsVisible" Converter="{StaticResource BoolToVisibilityConverter}" Mode="TwoWay"/>
</Window.Visibility>
I have a MVVM application and somewhere in the application our company use a Third-Party that cannot use {Binding}. It's a component that draw shapes, etc. What I want it, when the ViewModel load from the persisted storage all shapes to notify the View to draw them. In a perfect world I would just have the take the Third-party and bind it to the ViewModel Shapes collection but I cannot.
From there, my idea was that I could get from the View the ViewModel (via the DataContext) and to hook the PropertyChanged event. The problem is that the DataContext is not yet initialized in the constructor, so it's NULL, and I cannot hook the event. Here is a sample of the code:
public CanvasView()
{
InitializeComponent();
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged); //Exception Throw here because DataContext is null
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
How can I get information from my ViewModel to my View in that case?
All of the answers so far breaks the MVVM pattern with having code-behind on the view. Personally I would wrap the 3rd party control in a UserControl and then wire up a few dependency properties with property change events.
C#
public partial class MyWrappedControl : UserControl
{
public static readonly DependencyProperty ShapesProperty = DependencyProperty.Register("Shapes", typeof(ObservableCollection<IShape>), typeof(MyWrappedControl),
new PropertyMetadata(null, MyWrappedControl.OnShapesPropertyChanged);
public ObservableCollection<IShape> Shapes
{
get { return (ObservableCollection<IShape>)GetValue(ShapesProperty); }
set { SetValue(ShapesProperty, value); }
}
private static void OnShapesPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
((MyWrappedControl)o).OnShapesPropertyChanged(e);
}
private void OnShapesPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// Do stuff, e.g. shapeDrawer.DrawShapes();
}
}
XAML
<UserControl
Name="MyWrappedControl"
x:Class="MyWrappedControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!-- your control -->
<shapeDrawerControl x:Name="shapeDrawer" />
</UserControl>
you could also attach your handler in the Loaded event.
public CanvasView()
{
InitializeComponent();
this.Loaded += this.ViewLoaded;
}
void ViewLoaded(object sender, PropertyChangedEventArgs e)
{
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
I want to comment Dennis Roche answer.
Really, in this case we can use wrap approach, because we need to redraw view when Shapes collection changed. But view model logic can be too complex, and ,for instance, instead of redraw on PropertyChanged we should redraw on some custom event (f.i. ModelReloadEvent). In this case, wrapping doesn't help, but subscription on this event does, as in Muad'Dib solution - view model use event based communication with view, but this event should be view specific.
Using code-behind with View specific logic doesn't break MVVM. Yes, this code can be decorated with behavior/action, but using code behind - just simple solution.
Also, take a look at this view on MVVM. According to structure, ViewModel knows about abstract IView.If you worked with Caliburn/Caliburn.Micro MVVM frameworks you remember ViewAware class and IViewAware, which allows get view in view model.
So, more flexible solution I guess is next:
View:
public class CanvasView() : ICanvasView
{
public CanvasView()
{
InitializeComponent();
}
public void DrawShapes()
{
// implementation
}
}
ICanvasView:
public interface ICanvasView
{
void DrawShapes();
}
CanvasViewModel:
public class CanvasViewModel : ViewAware
{
private ObservableCollection<IShape> _shapes;
public ObservableCollection<IShape> Shapes
{
get
{
return _shapes;
}
set
{
_shapes = value;
NotifyOfPropertyChange(() => Shapes);
RedrawView();
}
}
private void RedrawView()
{
ICanvasView abstractView = (ICanvasView)GetView();
abstractView.DrawShapes();
}
}
Use the DataContextChanged event on the View (Window or UserControl)
public CanvasView()
{
InitializeComponent();
Action wireDataContext += new Action ( () => {
if (DataContext!=null)
((CanvasViewModel)this.DataContext).PropertyChanged += new PropertyChangedEventHandler(CanvasView_PropertyChanged);
});
this.DataContextChanged += (_,__) => wireDataContext();
wireDataContext();
}
void CanvasView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Shapes")
{
DrawShapes();
}
}
update: Here is a documented way to get DataContextChanged in Silverlight 3 and 4 http://www.lhotka.net/weblog/AddingDataContextChangedInSilverlight.aspx
I have a WPF application which is built on the MVVM design pattern.
I wish to implement a progress bar in the app, that follows the MVVM pattern.
Does any one have any suggestions on how to implement this?
Thanks in advance
Typically your UI would simply bind to properties in your VM:
<ProgressBar Value="{Binding CurrentProgress, Mode=OneWay}"
Visibility="{Binding ProgressVisibility}"/>
Your VM would use a BackgroundWorker to do the work on a background thread, and to periodically update the CurrentProgress value. Something like this:
public class MyViewModel : ViewModel
{
private readonly BackgroundWorker worker;
private readonly ICommand instigateWorkCommand;
private int currentProgress;
public MyViewModel()
{
this.instigateWorkCommand =
new DelegateCommand(o => this.worker.RunWorkerAsync(),
o => !this.worker.IsBusy);
this.worker = new BackgroundWorker();
this.worker.DoWork += this.DoWork;
this.worker.ProgressChanged += this.ProgressChanged;
}
// your UI binds to this command in order to kick off the work
public ICommand InstigateWorkCommand
{
get { return this.instigateWorkCommand; }
}
public int CurrentProgress
{
get { return this.currentProgress; }
private set
{
if (this.currentProgress != value)
{
this.currentProgress = value;
this.OnPropertyChanged(() => this.CurrentProgress);
}
}
}
private void DoWork(object sender, DoWorkEventArgs e)
{
// do time-consuming work here, calling ReportProgress as and when you can
}
private void ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.CurrentProgress = e.ProgressPercentage;
}
}
Use a ProgressBar control and bind its Value property to a property of the ViewModel:
View
<ProgressBar Minimum="0" Maximum="0" Value="{Binding CurrentProgress}" />
ViewModel
private double _currentProgress;
public double CurrentProgress
{
get { return _currentProgress; }
private set
{
_currentProgress = value;
OnPropertyChanged("CurrentProgress");
}
}
Add two properties to your VM:
bool IsProgressBarVisible
double ProgressValue
If you start a long time operation in your VM, set the IsProgressBarVisible-property to true and set the ProgressValue periodical to the current progress value. Try to calculate a value between 0 and 100. This has the advantage that you don't have to provide a minimum and maximum value.
After the asynchronous operation has completed, set the IsProgressBarVisible to false.
In XAML, bind to these two properties. Use a value converter to convert the boolean visibility to a Visibility.
<ProgressBar Value="{Binding ProgressValue}" Visibility="{Binding IsProgressBarVisible,Converter={StaticResource BooleanToVisibility_ValueConverter}}"/>