-
StringFormat bindings only work when the target type is string
Recently I had a problem putting some text into a WPF MenuItem. The binding in the Xaml was something like
<MenuItem Header="{Binding FileName, StringFormat='Open {0}'}" Command="{Binding Open}"/>`
It turned out that it wasn’t working because Header is a Content property, but the StringFormat binding extension only works when the target of the binding is of type string, such as TextBox.Text.
I’d never seen this mentioned anywhere before hitting this problem.So, for a MenuItem you need to use the HeaderStringFormat property instead. E.g.
<MenuItem Header="{Binding FileName}" HeaderStringFormat="Open {0}" Command="{Binding Open}"/>
For other controls you may need to set the Content property to a TextBlock and then bind to the Text property of that.
-
Using Rx to detect frozen UI
A common problem for some applications, and a very annoying thing for users, is when the UI thread is off doing some work that takes longer than expected – and it leaves the user unable to do anything because the application is frozen.
I wanted to detect when that happens in a desktop WPF app, which feels like a good fit for an Rx based solution.
First of all, we need to notice when our application is unresponsive. If our UI thread is free to process messages, then everything is fine. But if it takes too long to process them, then we have a frozen UI problem.
So if we periodically ping our UI thread and get an answer quickly, then everything is ok. If it’s not ok, we want to record that the UI thread hasn’t answered yet, and then wait for an answer later to tell us the UI has thawed.
Pinging the UI thread is straight forward enough with Rx. We need to set up an interval that will regularly produce values (we don’t care what the values are). Then pass those on to the UI thread. We’ll generate the values (i.e. the ping messages) on the task pool, and then watch for them on the dispatcher. Like so:
Observable.Interval(TimeSpan.FromSeconds(0.25)) .StartWith(0) .SubscribeOn(TaskPoolScheduler.Default) .ObserveOn(DispatcherScheduler.Current)
If our UI thread (i.e. the dispatcher) is busy, then
producedOnTaskPoolObservedOnUI
won’t be producing any values. If it goes for more than, say, one second without producing anything them we know our UI has become frozen. When our UI thread is no longer frozen, it’ll start producing values again.To do that, the full code becomes:
var producedOnTaskPoolObservedOnUI = Observable.Interval(TimeSpan.FromSeconds(0.25)) .StartWith(0) .SubscribeOn(TaskPoolScheduler.Default) .ObserveOn(DispatcherScheduler.Current) .Publish().RefCount(); producedOnTaskPoolObservedOnUI .Throttle(TimeSpan.FromSeconds(1)) .Window(producedOnTaskPoolObservedOnUI) .Subscribe(window => window.Subscribe(_ => { /* UI Is now frozen */ window.Subscribe( onNext: __ => { }, onCompleted: () => { /* UI is no longer frozen*/ }); }));
The
producedOnTaskPoolObservedOnUI
sequence, in an ideal world, will produce values every 250ms which means the Throttle of one second will never output anything (because the UI isn’t blocked). However, if we ever go for more than a second withoutproducedOnTaskPoolObservedOnUI
producing a value, the Throttle will let the most recent value through.When the Throttle does let a value through, we open an
IObservable
window (which is set to close the next timeproducedOnTaskPoolObservedOnUI
produces a value). Then we subscribe to that window, so the first value indicates that the UI is frozen. After we get the first value, we subscribe again to the window – this time we only care about the sequence completing, which tells us that the window has closed (becauseproducedOnTaskPoolObservedOnUI
produced a value) and we’re no longer frozen. We can’t just always subscribe toOnComplete
, because the Window() operator will produce empty windows if Throttle doesn’t let any values through.Now that we have “UI frozen” and “UI thawed” detection, we can do whatever we want at those points. Such as logging what the user was doing when we noticed the frozen UI.