I have a simple WPF window with: Loaded="StartTest"
and
<Grid>
<ListBox ItemsSource="{Binding Logging, IsAsync=True}"></ListBox>
</Grid>
In code behind I have in method StartTest:
LogModel LogModel = new LogModel();
void StartTest(object sender, RoutedEventArgs e)
{
DataContext = LogModel;
for (int i = 1; i<= 10; i++)
{
LogModel.Add("Test");
Thread.Sleep(100);
}
}
And class LogModel is:
public class LogModel : INotifyPropertyChanged
{
public LogModel()
{
Dispatcher = Dispatcher.CurrentDispatcher;
Logging = new ObservableCollection<string>();
}
Dispatcher Dispatcher;
public ObservableCollection<string> Logging { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Dispatcher.BeginInvoke((Action)delegate ()
{
Logging.Add(text);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Logging"));
});
}
}
Of course the problem is that the UI doesn't update in the loop.
What am I missing?
How can I achieve the UI update?
ObservableCollection already raises the PropertyChanged event when it's modified. You don't have to raise the event in the UI thread either.
Your model can be as simple as :
class LogModel
{
public ObservableCollection<string> Logging { get; } = new ObservableCollection<string>();
public void Add(string text)
{
Logging.Add(text);
}
}
All you need to do is set it as the DataContext of your view, eg :
LogModel model = new LogModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = model;
}
I assume StartTest is a click handler which means it runs on the UI thread. That means it will block the UI thread until the loop finishes. Once the loop finishes the UI will be updated.
If you want the UI to remain responsive during the loop, use Task.Delay instead of Thread.Slepp, eg :
private async void Button_Click(object sender, RoutedEventArgs e)
{
for(int i=0;i<10;i++)
{
await Task.Delay(100);
model.Add("Blah!");
}
}
Update
You don't need to use an ObservableCollection as a data binding source. You could use any object, including an array or List. In this case though you'd have to raise the PropertyChanged event in code :
class LogModel:INotifyPropertyChanged
{
public List<string> Logging { get; } = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Logging.Add(text);
PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
}
}
This will tell the view to load all the contents and display them again. This is perfectly fine when you only want to display data loaded eg from the database without modifying them, as it makes mapping entities to ViewModels a lot easier. In this case you only need to update the view when a new ViewModel is attached as a result of a command.
This is not efficient when you need to update the coolection though. ObservableCollection implements the INotifyCollectionChanged interface that raises an event for each change. If you add a new item, only that item will be rendered.
On the other hand you should avoid modifying the collection in tight loops because it will raise multiple events. If you load 50 new items, don't call Add 50 times in a loop. Create a new ObservableCollection, replace the old one and raise the PropertyChanged event, eg :
class LogModel:INotifyPropertyChanged
{
public ObservableCollection<string> Logging { get; set; } = new ObservableCollection<string>();
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Logging.Add(text);
PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
}
public void BulkLoad(string[] texts)
{
Logging = new ObservableCollection<string>(texts);
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Logging"));
}
}
The explicit implementation is still needed because the Logging property is getting replaced and can't raise any events itself
The reason why the UI is not updated in the loop is a call to Dispatcher.BeginInvoke. This places a new DispatcherOperation in the dispatcher queue. But your loop is already a dispatcher operation, and it continues on the Dispatcher's thread. So all the operations you queue will be executed after the loop's operation is finished.
Maybe you wanted to run the StartTest on a background thread? Then, the UI will update.
By the way, don't block the Dispatcher's thread with Thread.Sleep. It prevents the Dispatcher from doing its things as smoothly as possible.
It is the DoEvents thing, overhere:
public static void DoEvents()
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
new Action(delegate { }));
}
or even the perhaps better https://stackoverflow.com/a/11899439/138078.
Of course the test should be written differently in a way which does not require it.
Related
I have a WPF project and a Console one, the point of the WPF is to be the frontend UI and the console application is the logic that does the actual work.
In my backend I have a class with a method that does the work.
public static class BackendClass
{
public static void DoWork(ref string output)
{
//actual work
}
}
From the MVVM frontend my view model starts a task for this method and I want to be able to show status messages on the frontend about it. Things like "Started work.", "Doing so-and-so.", "Finished." and etc.
The code in my view model is:
class ViewModel : INotifyPropertyChanged
{
static string backendOutput;
public string BackendOutput
{
get => backendOutput;
set
{
if (backendOutput != value)
{
backendOutput = value;
OnPropertyChanged("BackendOutput");
}
}
}
public RelayCommand ExecuteCommand { get; private set; }
Task executionTask;
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel()
{
executionTask = new Task(() => BackendClass.DoWork(ref BackendOutput));
}
void OnExecute()
{
executionTask.Start();
ExecuteCommand.RaiseCanExecuteChanged();
}
bool CanExecute()
{
return (executionTask.Status != TaskStatus.Running &&
executionTask.Status != TaskStatus.WaitingToRun);
}
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The "BackendOutput" property is data binded to a text block in the WPF window.
I was thinking of passing the "BackendOutput" property so the "DoWork" method can append its status messages to it, thus raising the changed event, updating the frontend.
However if I try to assign it outside of the constructor I get the error that a property can't be a field initializer or something like that and in this case I get "property can't be passed as a ref parameter".
So how should I alert the frontend of what status messages the back is pumping?
ViewModel communicates with View via PropertyChanged event. So Model also can have an event. ViewModel subscribes to that event, updates property with event data, View gets updated.
Events are kind of protected delegates. So as a first step try to introduce a delegate:
public static void DoWork(Action<string> notifier)
{
notifier("output value");
}
and
executionTask = new Task(() => BackendClass.DoWork(str => { BackendOutput = str; }));
I created some test code so I could try and figure out how to use multiple windows in UWP properly. I wanted to see if I could fire an event and have multiple windows update their UI in the event handler. I finally got something working but I'm not entirely sure why it works.
Here's the class that's being created in my Page
public class NumberCruncher
{
private static Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>> StaticDispatchers { get; set; }
static NumberCruncher()
{
StaticDispatchers = new Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>>();
}
public NumberCruncher()
{
}
public event EventHandler<NumberEventArgs> NumberEvent;
public static void Register(int id, CoreDispatcher dispatcher, NumberCruncher numberCruncher)
{
StaticDispatchers.Add(id, new Tuple<CoreDispatcher, NumberCruncher>(dispatcher, numberCruncher));
}
public async Task SendInNumber(int id, int value)
{
foreach (var dispatcher in StaticDispatchers)
{
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
dispatcher.Value.Item2.NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
}
}
}
And here's the relevant part of my MainPage code
NumberCruncher numberCruncher;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NumberCruncher.Register(ApplicationView.GetForCurrentView().Id, Window.Current.Dispatcher, numberCruncher);
}
I have a button that creates new views of the MainPage. Then I have another button that calls the SendInNumber() method.
When I navigate to the MainPage I register the Dispatcher for the window and the instance of NumberCruncher. Then when firing the event I use the NumberCruncher EventHandler for that specific Dispatcher.
This works without throwing marshaling exceptions. If I try to use the current class's EventHandler
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
I get a marshaling exception when trying to add the item to the listView. However if I maintain the SynchronizationContext in my MainPage and then use SynchronizationContext.Post to update the listView. It works fine
SynchronizationContext synchronizationContext;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
synchronizationContext = SynchronizationContext.Current;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
synchronizationContext.Post(_ =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}, null);
}
However this does not work and throws a marshaling exception when trying to update listView.
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
await CoreApplication.GetCurrentView().CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
});
}
What is going on here?
Important thing to remember is that when an event is fired, the subscribed methods are called on the same thread as the Invoke method.
If I try to use the current class's EventHandler, I get a marshaling exception when trying to add the item to the listView.
This first error happens because you are trying to fire the event of current class on another Window's dispatcher (dispatcher.Value.Item1). Let's say the event is on Window 1 and the dispatcher.Value.Item1 belongs to Window 2. Once inside the Dispatcher block, you are or the UI thread of Window 2 and firing the NumberEvent of Window 1's NumberCruncher will run the Window 1's handler on the Window 2's UI thread and that causes the exception.
However this does not work and throws a marshaling exception when trying to update listView.
The GetCurrentView() method returns the currently active view. So whichever application view is active at that moment will be the one returned. In your case it will be the one on which you clicked the button. In case you call the Invoke method on the target Window's UI thread, you don't need any additional code inside the NumberEvent handler.
I have to update the text of a Label. I have bound the Text property of Label to a property and implemented INotifyPropertyChanged event.
My code is as follows:
public partial class MyClass : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _text;
public string ucText
{
get
{
return _text;
}
set
{
_text = value;
NotifyPropertyChanged("ucText");
}
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public MyClass()
{
InitializeComponent();
lblText.DataBindings.Add(new Binding("Text", this, "ucText"));
}
}
In a Button click event in another form, I update the text of the Label as follows:
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < 10000; i++)
{
myClass1.ucText = i.ToString();
}
}
Here myClass1 is object of the UserControl posted above.
In the Button click event, the UI hangs when updating the label and then once the loop completes, shows the final value:
9999
Why is my UI not reactive? I have also tried
lblText.DataBindings.Add(new Binding("Text", this, "ucText", false, DataSourceUpdateMode.OnPropertyChanged));
Both forms are running on the same thread, the UI thread. The following scenario is happening:
Button is clicked
Change text to i
Notify UI
Increment i
Go to 2. if i < 10000
Refresh the UI
As long as the loop isn't done, the UI thread won't redraw, as it's still doing some "heavy" work.
You can of course let a new thread handle the "calculation" and let that thread change the value. To start a new thread use either a backgroundworker or start a new thread with the Thread class.
The binding you are using is in fact working.
Edit: Always remember that all calculation that is directly done on the UI thread will block the UI for the time the calculation needs. Always use other threads to do time intensive calculations.
I've have created a IncrementalLoadingCollection class in my app which implements ISupportIncrementalLoading and inherits from ObservableCollection< T >.
It works fine and the items are loaded but I would like to show a message on the app's Status Bar that there is some work in progress.
What's a good way of achieving this?
Since LoadMoreItemsAsync is called internally when the list is scrolled, I cannot access that part to come up with the code which updates the Status Bar. Right now, I am doing this in LoadMoreItemsAsync which I find it a terrible approach, but I couldn't find a better one so far...
Any suggestions are highly appreciated.
Well you can for example: inherit from ObservableCollection and implement ISupportIncrementalLoading like this:
class IncrementalLoadingObservableCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{
private readonly Func<CancellationToken, Task<IEnumerable<T>>> _provideMoreItems;
public IncrementalLoadingObservableCollection(Func<CancellationToken, Task<IEnumerable<T>> provideMoreItems)
{
_provideMoreItems = provideMoreItems;
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
return AsyncInfo.Run(async cancelToken =>
{
await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnLoadMoreItemsStarted();
});
var providedItems = await _provideMoreItems(cancelToken);
await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
foreach(var item in providedItems)
Add(item);
OnLoadMoreItemsCompleted();
});
return new LoadMoreItemsResult {Count = (uint) providedItems.Count()};;
});
}
public bool HasMoreItems
{
get { return true; }
}
public event Action LoadMoreItemsStarted;
public event Action LoadMoreItemsCompleted;
protected virtual void OnLoadMoreItemsStarted()
{
var handler = LoadMoreItemsStarted;
if (handler != null) handler();
}
protected virtual void OnLoadMoreItemsCompleted()
{
var handler = LoadMoreItemsCompleted;
if (handler != null) handler();
}
}
How to use it? In your ViewModel:
class MyFancyItemsViewModel
{
public MyFancyItemsViewModel()
{
var incrementalObservablCollcetion = new IncrementalLoading...(GetItemsFromInternetOrSmth);
incrementalObservablCollcetion.LoadMoreItemsStarted += OnItemsLoadingStarted;
incrementalObservablCollcetion.LoadMoreItemsCompleted += OnItemsLoadingCompleted;
ItemsBindedInXaml = incrementalObservablCollcetion;
}
private Task<IEnumerable<Items>> GetItemsFromInternetOrSmth(CancellationToken cancelToken)
{
... do some work returns enumerable of Items
}
private void OnItemsLoadingStarted()
{ .. do smth .. }
private void OnItemsLoadingCompleted()
{ ... do smth .. }
public ObservableCollection<Items> ItemsBindedInXaml { get; private set; }
}
You might ask why I have used Dispatcher.RunAsync in IncrementalLoadingObservableCollection - the reason is that LoadMoreItemsAsync might run on another thread (don't know that) so you have to dispatch all the work to the UI Thread (it's not possible to call UI-related methods from thread other than UI thread without use of Dispatcher).
If you feel that ViewModel is not appropiate place for UI-related operations take a look at some messaging mechanisms (like MVVM Light Messenger, register message in code-behind and send this message in LoadMoreItemsStarted handler)
I have a custom control with itemsSource binding:
private void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
Results.Clear();
foreach (var check in newValue)
{
Results.Add(check as Check);
}
}
protected ObservableCollection<Check> results = new ObservableCollection<Check>();
public ObservableCollection<Check> Results
{
get { return results; }
set { results = value; }
}
Implemented in the main view:
<control:ResultCtrl x:Name="resultCtrl" ItemsSource="{Binding Path=Results, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"></control:ResultCtrl>
Check class:
public class Check : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected string checkStateString;
public string CheckStateString
{
get { return checkStateString; }
set
{
if (value != checkStateString)
{
checkStateString = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("CheckStateString"));
}
}
}
I call a class who calculate checks in the Main View show method:
Thread t = new Thread(new ThreadStart(
delegate
{
Dispatcher.Invoke(DispatcherPriority.Normal, new Action<ResultCtrl>(AddIn.Services.Results.Comprobaciones), resultCtrl);
}
));
t.Start();
In AddIn.Services.Results.Comprobaciones I do:
resultCtrl.ItemsSource = new ObservableCollection<Check>(AddIn.Document.Results);
for every check. Every time I do that I see how ItemsSource change, but Visual only update when the AddIn.Services.Results.Comprobaciones end. I tried to do UpdateLayout() and Items.Refresh() but nothing work.
Any ideas?
This code:
Thread t = new Thread(new ThreadStart(/* ... */);
t.Start();
creates a thread, that is completely useless, because everything it does is a blocking call to UI thread's Dispatcher. In other words, 99.999...% of time it runs on UI thread. You could easily write this:
AddIn.Services.Results.Comprobaciones();
with the same result.
You have to rewrite your code for having any benefits from multi-threading. I have no idea, how does your Comprobaciones method look like, but, obviously, you should call Dispatcher.Invoke only when you need to update something in UI.
Also note, that in most cases you shouldn't create Thread instances directly. Consider using TPL instead (possibly, via async/await, if you're targeting .NET 4.5).