How to update property in UI using MVVM in routine - c#

I want to build some count down counter. The problem is that my solution display only beginning value 10 and last value 1 after 10 sec. Of course, I've implemented the INotifyPropertyChanged interface. Any suggestions for that solution?
<Button Content="Generuj" Command="{Binding ButtonStart}"></Button>
<TextBox Text="{Binding Counter, Mode=OneWay}"></TextBox>
private void ButtonStartClick(object obj)
{
for (int i = 10; i > 0; i--)
{
System.Threading.Thread.Sleep(1000);
Counter = i;
}
}

With Thread.Sleep you are freezing your GUI. Try using a Timer for your purpose.
A timer will run simultaneously to your GUI thread and thus will not freeze it.
Also you will need to implement the PropertyChanged Event for your counter
Also make sure to set your DataContext
//create a dependency property you can bind to (put into class)
public int Counter
{
get { return (int)this.GetValue(CounterProperty); }
set { this.SetValue(CounterProperty, value); }
}
public static readonly DependencyProperty CounterProperty =
DependencyProperty.Register(nameof(Counter), typeof(int), typeof(MainWindow), new PropertyMetadata(default(int)));
//Create a timer that runs one second and decreases CountDown when elapsed (Put into click event)
Timer t = new Timer();
t.Interval = 1000;
t.Elapsed += CountDown;
t.Start();
//restart countdown when value greater one (put into class)
private void CountDown(object sender, ElapsedEventArgs e)
{
if (counter > 1)
{
(sender as Timer).Start();
}
Counter--;
}

You can also run Counter in separate Thread
Task.Run(() =>
{
for (int i = 10; i > 0; i--)
{
Counter = i;
Thread.Sleep(1000);
}
});

You can use async await to introduce a lightweight delay.
It's main advantage over a timer is there's no risk of leaving the delegate hooked and running.
In this viewmodel I use mvvmlight, but any implementation of ICommand would do.
…..
using System.Threading.Tasks;
using GalaSoft.MvvmLight.CommandWpf;
namespace wpf_99
{
public class MainWindowViewModel : BaseViewModel
{
private int counter =10;
public int Counter
{
get { return counter; }
set { counter = value; RaisePropertyChanged(); }
}
private RelayCommand countDownCommand;
public RelayCommand CountDownCommand
{
get
{
return countDownCommand
?? (countDownCommand = new RelayCommand(
async () =>
{
for (int i = 10; i > 0; i--)
{
await Task.Delay(1000);
Counter = i;
}
}
));
}
}
Not much to the view, it binds to Counter, of course:
<Grid>
<StackPanel>
<TextBlock Text="{Binding Counter}"/>
<Button Content="Count" Command="{Binding CountDownCommand}"/>
</StackPanel>
</Grid>

Related

How can I await for a button click in an async method?

I try to write a code to read a JSON File and allows user to input all the parametes for the objects in the JSON File one by one.
I try to write something like an "awaitable Button", but I failed to write a "GetAwaiter" extension for the button, although I found informations about how to do it.
https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-inherit-from-existing-windows-forms-controls?view=netframeworkdesktop-4.8
how can I combine await.WhenAny() with GetAwaiter extension method
http://blog.roboblob.com/2014/10/23/awaiting-for-that-button-click/
So here is my code after clicking a button "loadJSON":
for (int i = 0; i<templist_net.Count; i++)
{
GeneratorFunctions.GetNetworkParameterList(templist_net[i].Type, templist_net[i], treeViewPath.SelectedPath, SolutionFolder);
cBoxPouItem.Text = templist_net[i].Type;
ListViewParam2.ItemsSource = GeneratorFunctions.TempList; // Parameter list binding
temp = GeneratorFunctions.TempList;
ListViewParam2.Visibility = Visibility.Visible; // Set list 2 visible
ListViewParam.Visibility = Visibility.Collapsed; // Set list 1 invisible
//something stop loop, and wait user to type parameters in Listview, and click Button, Then the loop move on.
}
And Here is code trying to write a Button with extension. I add a new class for custom control, and write the extension.
public partial class CustomControl2 : System.Windows.Forms.Button
{
static CustomControl2()
{
}
public static TaskAwaiter GetAwaiter(this Button self)
{
ArgumentNullException.ThrowIfNull(self);
TaskCompletionSource tcs = new();
self.Click += OnClick;
return tcs.Task.GetAwaiter();
void OnClick(object sender, EventArgs args)
{
self.Click -= OnClick;
tcs.SetResult();
}
}
}
But I can't write a extension, which inherit System.Windows.Forms.Button. What should I do?
UPDATE:
here is what i tried.
private async Task Btn_loadJsonAsync(object sender, RoutedEventArgs e) {
// Initialize an open file dialog, whose filter has a extend name ".json"
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "(*.json)|*.json";
TextBoxInformation.Text += "Opening project ...\n";
if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
networks = GeneratorFunctions.ReadjsonNetwork(openFileDialog.FileName);
for (int i = 0; i < networks.Count; i++)
{
if (temp != null)
{
if (networks[i].Type == "Network")
{
templist_net.Add(networks[i]);
i = 1;
}
if (networks[i].Type == "Subsystem")
{
templist_sub.Add(networks[i]);
i = 1;
}
if (networks[i].Type == "Component: Data Point Based Control")
{
templist_com.Add(networks[i]);
i = 1;
}
}
}
using (SemaphoreSlim semaphore = new SemaphoreSlim(0, 1))
{
void OnClick(object sender, RoutedEventArgs e) => semaphore.Release();
btn.Click += OnClick;
for (int i = 0; i < templist_net.Count; i++)
{
//...
//wait here until [btn] is clicked...
await semaphore.WaitAsync();
}
btn.Click -= OnClick;
}}}
You can wait for a button click asynchronously using a SemaphoreSlim, e.g.:
using (SemaphoreSlim semaphore = new SemaphoreSlim(0, 1))
{
void OnClick(object sender, RoutedEventArgs e) => semaphore.Release();
btn.Click += OnClick;
for (int i = 0; i < templist_net.Count; i++)
{
//...
//wait here until [btn] is clicked...
await semaphore.WaitAsync();
}
btn.Click -= OnClick;
}
Although you may want to redesign the way you are doing things, a quick an dirty solution would be to use a dialog box in modal mode and upon the dialog box closing, capture the data that got input and continue looping. The loop will block until the dialog box is closed.
First of all I must insist that your request goes against the principles of the MVVM pattern which is based on events.
Your logic should be in a separate class and expose an OnNext method which should be called from the model through an ActionCommand
Anyway, to conform (as much as possible) to the MVVM pattern, you don't want to await on the button but more on the command bound to the button.
So let's build an awaitable command :
public class AwaitableCommand : ICommand
{
private readonly object _lock = new();
private TaskCompletionSource? _taskCompletionSource;
/// <summary>
/// null-event since it's never raised
/// </summary>
public event EventHandler? CanExecuteChanged
{
add { }
remove { }
}
/// <summary>
/// Always executable
/// </summary>
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
lock (_lock)
{
if (_taskCompletionSource is null)
return;
_taskCompletionSource.SetResult();
// reset the cycle
_taskCompletionSource = null;
}
}
public Task WaitAsync()
{
lock (_lock)
{
// start a new cycle if needed
_taskCompletionSource ??= new TaskCompletionSource();
return _taskCompletionSource.Task;
}
}
}
Then you can create your logic with it (I put it in the model, wich is a bad practice):
public class Model : NotifyPropertyChangedBase
{
private int _count;
public Model()
{
RunLogicAsync();
}
public int Count
{
get => _count;
private set => Update(ref _count, value);
}
public AwaitableCommand OnNextCommand { get; } = new();
/// <summary>
/// I know, I know, we should avoid async void
/// </summary>
private async void RunLogicAsync()
{
try
{
for (;;)
{
await OnNextCommand.WaitAsync();
Count++;
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
And your view:
<Window ...>
<Window.DataContext>
<viewModel:Model />
</Window.DataContext>
<Window.Resources>
<system:String x:Key="StringFormat">You clicked it {0} times</system:String>
</Window.Resources>
<Grid>
<Button Content="{Binding Count}"
ContentStringFormat="{StaticResource StringFormat}"
Command="{Binding OnNextCommand}"
Padding="10 5"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Window>
Working demo available here.

Using progress bar in WPF C# MVVM

My goal is to update the progress bar while another set of script (calculations) is running.
I have followed the sample files from here and tried to bind it to my MVVM script but the progress bar would not update.
Here is the Progressbar script
In the script below, I have included progressBarCounter and noOfDataas a value in another script that is calculated in a method.
Proof that data is updated
public partial class ProgressBarTaskOnWorkerThread : Window
{
public ProgressBarTaskOnWorkerThread()
{
InitializeComponent();
}
private void Window_ContentRendered(object sender, EventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerAsync();
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
EtabsDataFormatting.ViewModel.SpliceViewModel data = new EtabsDataFormatting.ViewModel.SpliceViewModel();
for (int i = data.progressBarCounter; i < data.noOfData;)
{
(sender as BackgroundWorker).ReportProgress(i);
}
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbStatus.Value = e.ProgressPercentage;
int perc = Convert.ToInt32(pbStatus.Value);
UpdateProgress(perc);
}
public void UpdateProgress(int percentage)
{
pbStatus.Value = percentage;
if (percentage == 100)
{
Close();
}
}
}
Here is part of my XAML code for the button to start calculations and run the progressbar
The command Binding = RunCalcBtn is bound to the calculation scripts, therefore, I have created a click to run the progress bar instead.
<Button x:Name = "ApplyButton" Margin="0 1 0 1" Content ="Start Calculation" Command="{Binding RunCalcBtn, Mode=TwoWay}" Click ="PrgBar_Click"/>
Progressbar XAML.cs button click
This part displays the progress bar, but it does not update.
private void PrgBar_Click(object sender, RoutedEventArgs e)
{
ProgressBar.ProgressBarTaskOnWorkerThread progressWindow = new ProgressBar.ProgressBarTaskOnWorkerThread();
progressWindow.Show();
}
Thank you so much for helping me in advance!
As Flithor has said, the best way to achieve this is with Progress<T>.
I give a short illustration of how to use this.
Firstly you need to create a Property in your View Model so that you can bind the ProgressBar's Value to something. Your ViewModel will need to implement INotifyPropertyChanged so that the Property set can invoke RaisePropertyChangedEvent.
Next create a Progress inside the method called by the Button click and pass it to your worker method. Use an ICommand for this, so that it can be bound to your Button (you don't need the Click event). Something like this:
var progress = new Progress<int>(percent =>
{
ProgressProperty = percent;
});
await Task.Run(() => myWorker(progress));
Finally within your worker method you periodically update the value like this:
private void myWorker(IProgress<int> progress)
{
progress.Report(1);
// ...
progress.Report(100);
}
By way of explanation: I used an integer, but you can also use a double if you want really fine calculations! The constructor of the Progress object takes the ProgressProperty (the name I gave to the property that gets bound to the ProgressBar) as a parameter. This means that when the worker calls Report(), the ProgressProperty is automatically updated with the new value, and hence can be reflected in the UI. Finally your worker method is invoked with await so that the UI is able to update on every incremented value.
For a very full explanation on Progress, see Stephen Cleary's blog
In MVVM WPF, you should do this to take full advantage of it:
View:
<Grid>
<ProgressBar Name="myProgressBar"
Minimum="0"
Value="{Binding ProgressBarValue,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
Maximum="100"
Foreground="{Binding ColorState,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
Background="#424242"
BorderBrush="Transparent"
BorderThickness="0"/>
<TextBlock Text="{Binding ElementName=myProgressBar, Path=Value,Mode=OneWay,UpdateSourceTrigger=PropertyChanged, StringFormat={}{0:0}%}"
FontWeight="DemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
namespace YourNameSpace.Models
{
public class Device : INotifyPropertyChanged
{
public Device()
{
this.ProgressBarValue = 50; // Your ProgressBar Foreground will be "GREEN" automatically
// This is the
}
private double progressBarValue;
public double ProgressBarValue
{
get { return progressBarValue; }
set
{
progressBarValue = value;
if(progressBarValue < 50)
this.ColorState = "Red";
else if (progressBarValue >= 50)
this.ColorState = "Green";
NotifyPropertyChanged("ProgressBarValue");
}
}
private string colorState = "Transparent";
public string ColorState
{
get { return colorState; }
set { colorState = value; NotifyPropertyChanged("ColorState"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string Obj)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(Obj));
}
}
}
}
You can REMOVE this from your code:
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbStatus.Value = e.ProgressPercentage;
int perc = Convert.ToInt32(pbStatus.Value);
UpdateProgress(perc);
}
public void UpdateProgress(int percentage)
{
pbStatus.Value = percentage;
if (percentage == 100)
{
Close();
}
}
And ONLY use this:
for (int i = data.progressBarCounter; i < 100; i++)
{
ProgressBarValue = i;
}
Your
ProgressBar Value
Progress Foreground Color
will be updated automatically.

How do I access a variable from another class to update my ViewModel

I have this application that has a ProgrressBar on it.
I also have a class which acts as a DownloadService because it's being used in multiple ViewModels.
As you can see the function DownloadData simulates it downloading something and keeps track of the total amount of data that has been downloaded in percent, it goes from 0 - 100.
How do I make my ProgressBar get that same value as totalDownloaded without passing in the MainWindowViewModel as a parameter.
MainWindow
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<ProgressBar Height="25"
Maximum="100"
Value="{Binding ProgressValue}"/>
<Button HorizontalAlignment="Center"
Width="100" Height="25"
Content="Start"
Command="{Binding StartDownloadCommand}"/>
</StackPanel>
MainWindowViewModel
class MainWindowViewModel : ObservableObject
{
private int _progressValue;
public int ProgressValue
{
get { return _progressValue; }
set
{
_progressValue = value;
OnPropertyChanged();
}
}
DownloadService ds = new DownloadService();
public RelayCommand StartDownloadCommand { get; set; }
public MainWindowViewModel()
{
StartDownloadCommand = new RelayCommand(o => { ds.DownloadData(); }, o => true);
}
}
And the DownloadService
class DownloadService
{
public void DownloadData()
{
int totalDownloaded = 0;
for (int i = 0; i < 101; i++)
{
totalDownloaded++;
Console.WriteLine(totalDownloaded);
}
}
}
Reference Progress<T> Class
Provides an IProgress<T> that invokes callbacks for each reported progress value.
Refactor the service to depend on IProgress<int> abstraction and use it to report progress
class DownloadService {
public void DownloadData(IProgress<int> progress = null) {
int totalDownloaded = 0;
for (int i = 0; i < 101; i++) {
totalDownloaded++;
progress?.Report(totalDownloaded); // ?. just in case
Console.WriteLine(totalDownloaded);
}
}
}
View model can pass a Progress<int> to the service to get reports of progress updates.
public MainWindowViewModel() {
StartDownloadCommand = new RelayCommand(o => {
var progress = new Progress<int>(value => ProgressValue = value);
ds.DownloadData(progress);
}, o => true);
}
explicitly depending on IProgress<int> avoids having to tightly couple the view model to the service.

Using a Stopwatch and DataBinding on a WPF Window that is already being updated using IProgress

In my Main() WPF program I run a time consuming method asynchronously. When this method is running, I fire up a secondary window that contains a ProgressBar, which I update using IProgress.
Following is an example of my setup.
MAIN Program:
public partial class MainWindow : Window
{
private ProgressBarWindow pbwWindow = null;
public MainWindow()
{
InitializeComponent();
}
private void RunMethodAsync(IProgress<int> progress)
{
Dispatcher.Invoke(() =>
{
pbwWindow = new ProgressBarWindow("Processing...");
pbwWindow.Owner = this;
pbwWindow.Show();
});
TimeConsumingMethod(progress);
}
private void TimeConsumingMethod(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
// Thread.Sleep() represents actual time consuming work being done.
Thread.Sleep(100);
progress.Report(i);
}
}
private async void btnRun_Click(object sender, RoutedEventArgs e)
{
IProgress<int> progress;
progress = new Progress<int>(i => pbwWindow.SetProgressUpdate(i));
await Task.Run(() => RunMethodAsync(progress));
}
}
My ProgressBarWindow which contains the progress bar looks like this:
public partial class ProgressBarWindow : Window
{
Stopwatch stopwatch = new Stopwatch();
BackgroundWorker worker = new BackgroundWorker();
public string ElapsedTimeString { get; set; }
public ProgressBarWindow(string infoText)
{
InitializeComponent();
SetTimer();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
StartTimer();
}
private void SetTimer()
{
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += (s, e) =>
{
while (!worker.CancellationPending)
{
worker.ReportProgress(0, stopwatch.Elapsed);
Thread.Sleep(1000);
}
};
worker.ProgressChanged += (s, e) =>
{
TimeSpan elapsedTime = (TimeSpan)e.UserState;
ElapsedTimeString = string.Format("{0}:{1}:{2}", elapsedTime.Minutes, elapsedTime.Seconds, elapsedTime.Milliseconds);
};
}
private void StartTimer()
{
stopwatch.Start();
worker.RunWorkerAsync();
}
private void StopTimer()
{
stopwatch.Stop();
worker.CancelAsync();
}
public void SetProgressUpdate(int progress)
{
pbLoad.Value = progress;
if (progress >= 100)
{
StopTimer();
Close();
}
}
}
I borrowed the StopWatch logic from this SO answer.
Then, on my ProgressBarWindow I have a TextBlock which I've used Binding as follows, just as the answer above says.
<TextBlock Name="tbElapsedTime" Text="{Binding ElapsedTimeString}"/>
Now when I run the program, the method executes, and the progress bar updates just fine. However, my TextBlock that's supposed to update with the elapsed time does not get updated.
To verify my timer's running fine, I updated TextBlock value directly as follows instead of Binding and it worked as expected and displayed Elapsed Time:
worker.ProgressChanged += (s, e) =>
{
TimeSpan elapsedTime = (TimeSpan)e.UserState;
ElapsedTimeString = string.Format("{0}:{1}:{2}", elapsedTime.Minutes, elapsedTime.Seconds, elapsedTime.Milliseconds);
tbElapsedTime.Text = ElapsedTimeString;
};
So I'm guessing my problem is with the Binding and possibly using BackgroundWorker on a windows that's already being run asynchronously? How could I fix this so I could use DataBinding?
As mentioned by Ginger Ninja, you have to implement INotifyPropertyChanged and use RelativeSource={RelativeSource Self} (as additional setting to the binding):
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _ElapsedTimeString;
public string ElapsedTimeString
{
get { return _ElapsedTimeString; }
set
{
if (_ElapsedTimeString != value)
{
_ElapsedTimeString = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ElapsedTimeString"));
}
}
}
// ....
}
and the XAML:
<TextBlock Name="tbElapsedTime" Text="{Binding ElapsedTimeString, RelativeSource={RelativeSource Self}}"/>
Data binding is often used in combination with MVVM. That is IMHO the prefered way to solve your problem... If you want to use MVVM, you have to implement a view model that contains all the logic and implements INotifyPropertyChanged. Than you can simply bind properties from the view model to the view. That ensures a nice separation between (GUI related) logic and view.

How to update ListBox when ObservableCollection items changed?

I have test app which adding items to ListBox in loop. Now, ListBox updates, when all items added to ObservableCollection. I need to update ListBox when every item added. Please, help russian guy =)
Here is my code:
MainWindow.xaml
<Grid>
<Button Content="Button" HorizontalAlignment="Left" Margin="432,288.04,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ListBox x:Name="urlsListBox" ItemsSource="{Binding Urls}" HorizontalAlignment="Left" Height="300" Margin="10,10,0,0" VerticalAlignment="Top" Width="417"/>
</Grid>
MainWindow.xaml.cs
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ViewModel model = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = model;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
model.GetUrls();
}
}
ViewModel.cs
class ViewModel
{
public ObservableCollection<Url> Urls { get; set; }
public ViewModel()
{
Urls = new ObservableCollection<Url>();
}
public void GetUrls()
{
for (int i = 0; i < 5; i++)
{
Urls.Add(new Url { link = i.ToString() });
//Thread.Sleep(300);
}
}
}
public class Url
{
public string link { get; set; }
}
I think you need to notify the UI about the change happening in the URL observable collection each time it adds a new item.
If you have implemented INotifyPropertyChanged in your view model then register the observable collection with any change happening in it.
private ObservableCollection<Url> _urls;
public ObservableCollection<Url> Urls
{ get
{
return _urls;
}
set
{
_urls = value;
OnPropertyModified("Urls");
}
}
You want UI to update on every item addition in collection i guess.
Looking at Thread.Sleep(300), i am assuming you want to wait for 300 milliseconds before adding another item to collection.
DispatcherTimer is what you are looking for then. This sample will work:
public void GetUrls()
{
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 0, 0, 300);
int i = 0;
timer.Tick += (s, e) =>
{
Urls.Add(new Url { link = i.ToString() });
++i;
if (i == 5)
timer.Stop();
};
timer.Start();
}
This will add item in collection after every 300 ms and will stop timer once item count reaches to 5.
Like i mentioned in comment
Dispatcher associated with UI thread can process one operation at a
time. When it's adding item to collection, it can't process Rendering
operation at same time. Hence GUI not getting refreshed. Usage of DispatcherTimer gives some time to rendering operations to get processed.
Another approach without DispatcherTimer would be to enqueue empty delegate on UI dispatcher with Render priority so that all operations with priority higher than Render (including Render) gets processed before moving to addition of another item in collection.
public void GetUrls()
{
for (int i = 0; i < 5; i++)
{
Urls.Add(new Url { link = i.ToString() });
App.Current.Dispatcher.Invoke((Action)(() => { }),
DispatcherPriority.Render);
}
}

Categories