I'm trying to update a progress bar from two separate tasks at the same time. I've tried it a number of ways but haven't had much success. See below full code example. In WPF, add a progressbar, bind Maximum to TextMax and Value to TextProgress and you will notice that the progress bar only fills to about half way.
NOTE: This is not my actual solution, just a sample of what I am doing that shows the issue that I threw together, please ignore code style/pattern issues.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace DelegateTesting
{
public partial class MainWindow : Window
{
ResultsItemViewModel _ViewModel = new ResultsItemViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = _ViewModel;
TextProcessing();
}
private static void Method1(
Action<int> reportProgress)
{
var progress = 0;
for(int i = 0;i<100;i++)
{
//Thread.Sleep(200);
reportProgress?.Invoke(++progress);
}
}
private static void Method2(
Action<int> reportProgress)
{
var progress = 0;
for (int i = 0; i < 100; i++)
{
//Thread.Sleep(200);
reportProgress?.Invoke(++progress);
}
}
private async Task TextProcessing()
{
_ViewModel.TextMax += 100;
_ViewModel.TextMax += 100;
var dispatcher = Application.Current.Dispatcher;
var reportProgress = dispatcher.MakeInvoker<int>(p => _ViewModel.TextProgress = p);
await Task.WhenAll(
Task.Run(() => Method1(reportProgress)),
Task.Run(() => Method2(reportProgress)));
}
}
public static class DispatcherHelper
{
public static Action<T> MakeInvoker<T>(
this Dispatcher dispatcher,
Action<T> action,
DispatcherPriority priority = DispatcherPriority.Normal)
{
return o => dispatcher.BeginInvoke(priority, action, o);
}
}
public class ResultsItemViewModel : INotifyPropertyChanged
{
int _textProgress, _textMax;
public int TextProgress
{
get => _textProgress;
set
{
_textProgress = value;
NotifyPropertyChanged();
}
}
public int TextMax
{
get => _textMax;
set
{
_textMax = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You are not awaiting TextProcessing. You cannot just call await in the middle. You can either go asynchronous all the way or not at all.
Also, doing work in a constructor is not a good practice.
In order for this to work you have to allow WPF to handle async calls for you. I assume you want to start an action when someone does something, e.g.:
protected async void Button_OnClick(Object sender, EventArgs e)
{
await TextProcessing();
}
You could always bind it to an event that starts a window or something like that.
To be honest your code is very unclear to me, so this might help you understand what you actually need:
int _textProgress, _textMax;
public int TextProgress
{
get => _textProgress;
set
{
_textProgress = value;
NotifyPropertyChanged();
}
}
public int TextMax
{
get => _textMax;
set
{
_textMax = value;
NotifyPropertyChanged();
}
}
protected async void Button_OnClick(Object sender, EventArgs e)
{
TextMax = 0;
var t1 = Task.Run(() => {
TextProgress += 50;
});
var t2 = Task.Run(() => {
TextProgress += 50;
});
await Task.WhenAll(t1, t2);
}
And in your view you should have some kind of button with command for Button_OnClick and progress bar:
<ProgressBar Maximum="100" Height="50" Value="{Binding TextProgress}"></ProgressBar>
One more thing. It looks like you have a ViewModel, but you are doing work in a view. You should move that logic to your ViewModel.
Related
I'm having issues updating the UI threads. Application is running 1 UI thread for each form, meaning just using SyncronizationContext with the UI thread doesn't work. I'm doing this for looping update performance as well as modal popup possibilities like select value before you can use the form.
How I'm creating it in ApplicationContext:
public AppContext()
{
foreach(var form in activeForms)
{
form.Load += Form_Load;
form.FormClosed += Form_FormClosed;
StartFormInSeparateThread(form);
//form.Show();
}
}
private void StartFormInSeparateThread(Form form)
{
Thread thread = new Thread(() =>
{
Application.Run(form);
});
thread.ApartmentState = ApartmentState.STA;
thread.Start();
}
There are controls on each for that are databound and updating with values from the same databound object. Controls being Labels and DataGridview (bound to a bindinglist).
What would be ideal is having the Bindinglist threadsafe and execute on these multiple UI threads. Found some examples that I attempted like this:
List<SynchronizationContext> listctx = new();
public ThreadSafeBindingList2()
{
//SynchronizationContext ctx = SynchronizationContext.Current;
//listctx.Add(ctx);
}
public void SyncContxt()
{
SynchronizationContext ctx = SynchronizationContext.Current;
listctx.Add(ctx);
}
protected override void OnAddingNew(AddingNewEventArgs e)
{
for (int i = 0; i < listctx.Count; i++)
{
if (listctx[i] == null)
{
BaseAddingNew(e);
}
else
{
listctx[i].Send(delegate
{
BaseAddingNew(e);
}, null);
}
}
}
void BaseAddingNew(AddingNewEventArgs e)
{
base.OnAddingNew(e);
}
protected override void OnListChanged(ListChangedEventArgs e)
{
for (int i = 0; i < listctx.Count; i++)
{
if (listctx[i] == null)
{
BaseListChanged(e);
}
else
{
listctx[i].Send(delegate
{
BaseListChanged(e);
}, null);
}
}
}
void BaseListChanged(ListChangedEventArgs e)
{
base.OnListChanged(e);
}
I'm also using a static class as a data property change hub for all controls so I don't change the databinding source more than once (again due to performance), where I have a background worker "ticking" every 1-3 seconds depending on system load:
private static void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e)
{
if (timerStart is false)
{
Thread.Sleep(6000);
timerStart = true;
}
while (DisplayTimerUpdateBGW.CancellationPending is false)
{
//UIThread.Post((object stat) => //Send
//{
threadSleepTimer = OrderList.Where(x => x.Status != OrderOrderlineStatus.Claimed).ToList().Count > 20 ? 2000 : 1000;
if (OrderList.Count > 40)
threadSleepTimer = 3000;
UpdateDisplayTimer();
//}, null);
Thread.Sleep(threadSleepTimer);
}
}
private static void UpdateDisplayTimer()
{
var displayLoopStartTimer = DateTime.Now;
TimeSpan displayLoopEndTimer = new();
Span<int> orderID = CollectionsMarshal.AsSpan(OrderList.Select(x => x.ID).ToList());
for (int i = 0; i < orderID.Length; i++)
{
OrderModel order = OrderList[i];
order.OrderInfo = "Ble";
Span<int> OrderLineID = CollectionsMarshal.AsSpan(order.Orderlines.Select(x => x.Id).ToList());
for (int j = 0; j < OrderLineID.Length; j++)
{
OrderlineModel ol = order.Orderlines[j];
TimeSpan TotalElapsedTime = ol.OrderlineCompletedTimeStamp != null ? (TimeSpan)(ol.OrderlineCompletedTimeStamp - ol.OrderlineReceivedTimeStamp) : DateTime.Now - ol.OrderlineReceivedTimeStamp;
string displaytimerValue = "";
if (ol.OrderlineCompletedTimeStamp == null)
displaytimerValue = TotalElapsedTime.ToString(#"mm\:ss");
else
displaytimerValue = $" {(DateTime.Now - ol.OrderlineCompletedTimeStamp)?.ToString(#"mm\:ss")} \n({TotalElapsedTime.ToString(#"mm\:ss")})";
ol.DisplayTimer = displaytimerValue;
}
}
}
Ideally I want to have the labels and datagridview properties databindings so that I can have INotifyPropertyChanged just updating these relevant properties in all UI threads.
Any help would be appreciated!
One of many ways to look at this is that there's only one display area (albeit which might consist of many screens) and only one element of it can change at any given moment. To my way of thinking, this means that having more than one UI thread can often be self defeating (unless your UI is testing another UI). And since the machine has some finite number of cores, having a very large number of threads (whether of the UI or worker variety) means you can start to have a lot of overhead marshalling the context as threads switch off.
If we wanted to make a Minimal Reproducible Example that has 10 Form objects executing continuous "mock update" tasks in parallel, what we could do instead of the "data property change hub" you mentioned is to implement INotifyPropertyChanged in those form classes with static PropertyChanged event that gets fired when the update occurs. To mock data binding where FormWithLongRunningTask is the binding source, the main form subscribes to the PropertyChanged event and adds a new Record to the BindingList<Record> by identifying the sender and inspecting e to determine which property has changed. In this case, if the property is TimeStamp, the received data is marshalled onto the one-and-only UI thread to display the result in the DataGridView.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Subscribe to the static event here.
FormWithLongRunningTask.PropertyChanged += onAnyFWLRTPropertyChanged;
// Start up the 10 forms which will do "popcorn" updates.
for (int i = 0; i < 10; i++)
{
new FormWithLongRunningTask { Name = $"Form{i}" }.Show(this);
}
}
private void onAnyFWLRTPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is FormWithLongRunningTask form)
{
BeginInvoke(() =>
{
switch (e.PropertyName)
{
case nameof(FormWithLongRunningTask.TimeStamp):
dataGridViewEx.DataSource.Add(new Record
{
Sender = form.Name,
TimeStamp = form.TimeStamp,
});
break;
default:
break;
}
});
}
}
}
The DataGridView on the main form uses this custom class:
class DataGridViewEx : DataGridView
{
public new BindingList<Record> DataSource { get; } = new BindingList<Record>();
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!DesignMode)
{
base.DataSource = this.DataSource;
AllowUserToAddRows = false;
#region F O R M A T C O L U M N S
DataSource.Add(new Record());
Columns[nameof(Record.Sender)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
var col = Columns[nameof(Record.TimeStamp)];
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
col.DefaultCellStyle.Format = "hh:mm:ss tt";
DataSource.Clear();
#endregion F O R M A T C O L U M N S
}
}
protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
{
base.OnCellPainting(e);
if ((e.RowIndex > -1) && (e.RowIndex < DataSource.Count))
{
var record = DataSource[e.RowIndex];
var color = _colors[int.Parse(record.Sender.Replace("Form", string.Empty))];
e.CellStyle.ForeColor = color;
if (e.ColumnIndex > 0)
{
CurrentCell = this[0, e.RowIndex];
}
}
}
Color[] _colors = new Color[]
{
Color.Black, Color.Blue, Color.Green, Color.LightSalmon, Color.SeaGreen,
Color.BlueViolet, Color.DarkCyan, Color.Maroon, Color.Chocolate, Color.DarkKhaki
};
}
class Record
{
public string Sender { get; set; } = string.Empty;
public DateTime TimeStamp { get; set; }
}
The 'other' 10 forms use this class which mocks a binding source like this:
public partial class FormWithLongRunningTask : Form, INotifyPropertyChanged
{
static Random _rando = new Random(8);
public FormWithLongRunningTask() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
_ = runRandomDelayLoop();
}
private async Task runRandomDelayLoop()
{
while(true)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(_rando.NextDouble() * 10));
TimeStamp = DateTime.Now;
Text = $"# {TimeStamp.ToLongTimeString()}";
BringToFront();
}
catch (ObjectDisposedException)
{
}
}
}
DateTime _timeStamp = DateTime.Now;
public DateTime TimeStamp
{
get => _timeStamp;
set
{
if (!Equals(_timeStamp, value))
{
_timeStamp = value;
OnPropertyChanged();
}
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
add => PropertyChanged += value;
remove => PropertyChanged -= value;
}
public static event PropertyChangedEventHandler? PropertyChanged;
}
I believe that there's no 'right' answer to your question but I hope there's something here that might move things forward for you.
I'm trying to learn the MVVM structure. How can I update a variable that changes constantly in another class in the UI.
I created a simple example because the project codes are too much. But I failed.
I would be very grateful if you could tell me where I went wrong. Thanks.
MyModel
public class Temperature : INotifyPropertyChanged
{
private double _memsTemperature;
private double _cpuTemperature;
private double _animalTemperature;
public double MemsTemperature
{
get { return _memsTemperature; }
set
{
_memsTemperature = value;
OnPropertyChanged("MemsTemperature");
}
}
public double CpuTemperature
{
get { return _cpuTemperature; }
set
{
_cpuTemperature = value;
OnPropertyChanged("CpuTemperature");
}
}
public double AnimalTemperature
{
get { return _animalTemperature; }
set
{
_animalTemperature = value;
OnPropertyChanged("AnimalTemperature");
}
}
System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
public Temperature()
{
dispatcherTimer.Tick += DispatcherTimer_Tick;
dispatcherTimer.Interval = TimeSpan.FromSeconds(1);
dispatcherTimer.Start();
}
private void DispatcherTimer_Tick(object sender, System.EventArgs e)
{
MemsTemperature = MemsTemperature + 1;
CpuTemperature = CpuTemperature + 2;
AnimalTemperature = AnimalTemperature + 3;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
MainWindowViewModel
public class MainWindowViewModel
{
public double MemTemp { get; set; }
public MainWindowViewModel()
{
MemTemp = new Temperature().MemsTemperature;
}
}
Main Window Xaml and C# Code
<TextBlock Text="{Binding MemTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
The MainWindowViewModel should expose a Temperature property, e.g. like this:
public class MainWindowViewModel
{
public Temperature Temperature { get; } = new Temperature();
}
and the Binding should then look like this:
<TextBlock Text="{Binding Temperature.MemsTemperature}"/>
Neither Mode=TwoWay nor UpdateSourceTrigger=PropertyChanged makes sense on the Binding of a TextBlock's Text property.
The OnPropertyChanged method would simpler and safer be implemented like this:
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
You have a XAML page with UI controls that bind to those constantly-changing properties. When you send out the PropertyChanged notifications, the UI control will automatically update itself.
The problem with the code you wrote is that you never bound to the actual temperature. XAML doesn't know how to translate MemTemp into anything other than it's name unless you write a DataTemplate for it.
For example, (assuming a grid) something like this:
<TextBlock Grid.Row="0" Grid.Column="0" Text="Animal: "/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding MemTemp.AnimalTemperature}"/>
I would define an explicit worker class which performs the measurements. This class
has an event (OnMeasurement), which can be subscribed in the ViewModel:
// Arguments for the mesurement event (temperature, ...)
public class MeasurementEventArgs : EventArgs
{
public double Temperature { get; }
public MeasurementEventArgs(double temperature)
{
Temperature = temperature;
}
}
public class MeasurementWorker
{
private readonly CancellationTokenSource _tcs = new CancellationTokenSource();
// Provides an event we can subscribe in the view model.
public event Action<object, MeasurementEventArgs> OnMeasurement;
public void Stop()
{
_tcs.Cancel();
}
// Measurement routine. Perform a measurement every second.
public async Task Start()
{
try
{
var rnd = new Random();
while (!_tcs.IsCancellationRequested)
{
var temperature = 20 * rnd.NextDouble();
OnMeasurement?.Invoke(this, new MeasurementEventArgs(temperature));
await Task.Delay(1000, _tcs.Token);
}
}
catch (TaskCanceledException) { }
// TODO: Create an error event to catch exceptions from here.
catch { }
}
}
In your MainWindow class you instantiate your viewmodel and your worker:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel(new MeasurementWorker());
}
// Register in XAML with <Window ... Closing="StopMeasurement">
public async void StopMeasurement(object sender, System.ComponentModel.CancelEventArgs e)
{
var vm = DataContext as MainWindowViewModel;
await vm.StopMeasurement();
}
}
In your view model you can subscribe to the worker event and raise OnPropertyChanged in your callback function:
public class MainWindowViewModel : INotifyPropertyChanged
{
private double _memsTemperature;
private readonly MeasurementWorker _mw;
private readonly Task _measurementWorkerTask;
public double MemsTemperature
{
get => _memsTemperature;
set
{
_memsTemperature = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MemsTemperature)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void ProcessMeasurement(object sender, MeasurementEventArgs args)
{
MemsTemperature = args.Temperature;
}
// You can call this if you want to stop your measurement. Should be called if you close your app.
public async Task StopMeasurement()
{
_mw.OnMeasurement -= ProcessMeasurement;
_mw.Stop();
// Clean shutdown
await _measurementWorkerTask;
}
public MainWindowViewModel(MeasurementWorker mw)
{
_mw = mw;
_mw.OnMeasurement += ProcessMeasurement;
_measurementWorkerTask = _mw.Start();
}
}
I am trying to figure out how to update my main UI (for example a textblock) from another thread. The only way I have been able to do it so far is using the Progress object. I have a situation where I cannot use the Progress object and I was recently pointed to using the MVVM / binding method. I have watch a few videos and done some examples but I can't seem to get it to work.
<TextBlock Name="txtblock1" Text="{Binding count}"></TextBlock>
Here are my errors
Exception thrown: 'System.Runtime.InteropServices.COMException' in
System.Runtime.WindowsRuntime.dll Exception thrown:
'System.Runtime.InteropServices.COMException' in mscorlib.ni.dll
Exception thrown: 'System.Runtime.InteropServices.COMException' in
mscorlib.ni.dll
View (Code behind)
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
ViewModelexample obj = new ViewModelexample();
txtblock1.DataContext = obj;
obj.Methodasync();
}
}
ViewModel
public class ViewModelexample : INotifyPropertyChanged
{
public string count { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void onPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler !=null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public async void Methodasync()
{
await Method();
}
public Task Method()
{
return Task.Factory.StartNew(() =>
{
for (int i = 0; i < 100; i++)
{
Task.Delay(1000).Wait();
Debug.WriteLine(i.ToString());
count = i.ToString();
onPropertyChanged(i.ToString());
}
});
}
}
Any ideas on what I might be doing wrong?
Thank you
Your count Property needs to be able to notify that it has changed
public class ViewModelexample : INotifyPropertyChanged
{
private string _count;
public string count {
get { return _count; }
set {
if(value != _count) {
_count = value;
OnPropertyChanged(nameof(count));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
// the new Null-conditional Operators are thread-safe:
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int _testCount = 0;
public void Method() {
testCount++;
Debug.WriteLine(testCount.ToString());
count = testCount.ToString();
}
}
The above works because the new Null-conditional Operators are thread-safe:
Another use for the null-condition member access is invoking delegates
in a thread-safe way with much less code. ... The new way is
thread-safe because the compiler generates code to evaluate
PropertyChanged one time only, keeping the result in a temporary
variable.
To Test it you can edit your ViewModel method and have the view call it on an event like on page loaded or a button click.
public sealed partial class MainPage : Page
{
ViewModelexample obj = null;
public MainPage()
{
this.InitializeComponent();
obj = new ViewModelexample();
this.DataContext = obj;
}
public void OnSomeEventHandler() {
obj.Method();
}
}
I was under the assumption that binding would take care of the cross threading call.
No, this is only true for some MVVM frameworks (such as WPF). For this reason, I prefer to treat all my ViewModels as belonging to the UI.
With your code sample, you should be able to use Progress<T>:
public async void Methodasync()
{
var progress = new Progress<int>(value =>
{
count = value;
onPropertyChanged("count");
});
await Method(progress);
}
public Task Method(IProgress<int> progress)
{
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Task.Delay(1000).Wait();
Debug.WriteLine(i.ToString());
progress.Report(i);
}
});
}
Also note that I changed from StartNew to Run. As a general rule, don't use StartNew for reasons I describe on my blog.
If you "really for serious" cannot use Progress<T> for some odd reason, then you can use Reactive Extensions or SynchronizationContext directly.
Example using SynchronizationContext:
public Task Method()
{
var context = SynchronizationContext.Current;
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Task.Delay(1000).Wait();
Debug.WriteLine(i.ToString());
var localI = i;
context.Post(() =>
{
count = localI;
onPropertyChanged("count");
});
}
});
}
The localI thing is to avoid closing over the loop variable.
For example I have something like this. When I am clicking on first button it start's async process and then I am clicking second button it start's second process. But I need only one process to work after clicking on each button. How can I cancel other process?
namespace WpfApplication55
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
TestCombo TC = new TestCombo();
public MainWindow()
{
DataContext = TC;
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TC.Begin(60);
}
private void Button_Click1(object sender, RoutedEventArgs e)
{
TC.Begin(120);
}
}
public class TestCombo:INotifyPropertyChanged
{
private int someData;
public int SomeData
{
get { return someData; }
set { someData = value; RaisePropertyChanged("SomeData"); }
}
public void StartCount(int input)
{
SomeData = input;
while (input>0)
{
System.Threading.Thread.Sleep(1000);
input -= 1;
SomeData = input;
}
}
public void Begin(int input)
{
Action<int> Start = new Action<int>(StartCount);
IAsyncResult result = Start.BeginInvoke(input, null, null);
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged (string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
I'm not exactly sure how you want the while condition in StartCount to work but as long as you evaluating the new CancellationToken you should be good to cancel. Remember the Thread.Sleep won't cancel while its sleeping. So you may have up to a 1s delay.
public void StartCount(int input, CancellationToken token)
{
SomeData = input;
while (input > 0 && !token.IsCancellationRequested)
{
System.Threading.Thread.Sleep(1000);
input -= 1;
SomeData = input;
}
}
IAsyncResult process;
public void Begin(int input)
{
if (process != null && !process.IsCompleted)
((CancellationTokenSource)process.AsyncState).Cancel();
Action<int, CancellationToken> Start = new Action<int, CancellationToken>(StartCount);
var cancelSource = new CancellationTokenSource();
process = Start.BeginInvoke(input,cancelSource.Token, null, cancelSource);
}
I would use Microsoft's Reactive Framework for this.
Here's your class:
public class TestCombo : INotifyPropertyChanged
{
private int someData;
public int SomeData
{
get { return someData; }
set { someData = value; RaisePropertyChanged("SomeData"); }
}
private SingleAssignmentDisposable _subscription = new SingleAssignmentDisposable();
public void Begin(int input)
{
_subscription.Disposable =
Observable
.Interval(TimeSpan.FromSeconds(1.0))
.Select(x => input - (int)x)
.Take(input)
.ObserveOnDispatcher()
.Subscribe(x => this.SomeData = x);
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged (string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
The two key parts to this solution is first the observable query subscription which does all of the timing, computes the value to assign to SomeData and marshals the assignment to the UI thread.
The second is the SingleAssignmentDisposable. When you assign a new IDisposable to its Disposable property it will dispose any previously assigned IDisposable.
The disposing cancels the previous subscription.
Just NuGet "Rx-WPF" to get the WPF bits for Rx.
Try something like this:
namespace WpfApplication55
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
TestCombo TC = new TestCombo();
CancellationTokenSource cts;
public MainWindow()
{
DataContext = TC;
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
cts = new CancellationTokenSource();
await TC.DoAsync(60, cts.Token);
}
private void Button_Click1(object sender, RoutedEventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
cts = new CancellationTokenSource();
await TC.DoAsync(120, cts.Token);
}
}
public class TestCombo:INotifyPropertyChanged
{
private int someData;
public int SomeData
{
get { return someData; }
set { someData = value; RaisePropertyChanged("SomeData"); }
}
public void StartCount(int input)
{
SomeData = input;
while (input>0)
{
System.Threading.Thread.Sleep(1000);
input -= 1;
SomeData = input;
}
}
public Task DoAsync(int input, CancellationToken cancellationToken)
{
return Task.Run(StartCount, cancellationToken);
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged (string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Try using class CancellationTokenSource;
See code below-
CancellationTokenSource ctstask = new CancellationTokenSource();
ctstask.Cancel();//This line should be called from 2nd button click.
I dont know if it possible but what I want is something like that
In WinForm listbox1 has a list of lines(copied from file)
In another Thread and class I run on a List that contains the same lines each line I parse and DoSomething
once I finish with that line I want the index in the listbox to change
from my basic and limited understanding my approach was with an Event to fire in form and than maybe using Invoke (for not to cross thread )
Is there is a way to somehow bind to index of the listbox somehow with my for/foreach loop ?
class form
{
listBoxScript.SetSelected(ScriptCounter, true);<--bind the ScriptCounter?
}
in another Class
class RunScript
{
//..
public void RunScriptList()
{
ScriptCounter = 0 ;
foreach ( var cell in ScriptList)
{
ScriptCounter ++;
//DoSomething
}
}
}
Make sure you implement INotifyPropertyChanged in RunScript class. Here's a complete sample:
class RunScript : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int scriptCounter;
private ISynchronizeInvoke invoker;
public RunScript(ISynchronizeInvoke invoker)
{
if (invoker == null) throw new ArgumentNullException("invoker");
this.invoker = invoker;
}
public async void RunScriptList()
{
ScriptCounter = 0;
foreach (var cell in Enumerable.Range(1, 15))
{
ScriptCounter++;
await Task.Delay(1000);
//DoSomething
}
}
public int ScriptCounter
{
get { return scriptCounter; }
set
{
scriptCounter = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
Action action = () => handler(this, new PropertyChangedEventArgs(propertyName));
invoker.Invoke(action, null);
}
}
}
private RunScript rs;
public Form1()
{
InitializeComponent();
rs = new RunScript(this)
var binding = new Binding("SelectedIndex", rs, "ScriptCounter", false, DataSourceUpdateMode.OnPropertyChanged);
listBox1.DataBindings.Add(binding);
}
private void button1_Click(object sender, EventArgs e)
{
rs.RunScriptList();
}
Note I have used async/await in RunScriptList, If you do it in another thread you need to fire PropertyChanged event in main thread to avoid cross thread exception.