11

Suppose I have a modal dialog with a textbox and OK/Cancel buttons. And it is built on MVVM - i.e. it has a ViewModel object with a string property that the textbox is bound to.

Say, I enter some text in the textbox and then grab my mouse and click "OK". Everything works fine: at the moment of click, the textbox loses focus, which causes the binding engine to update the ViewModel's property. I get my data, everybody's happy.

Now suppose I don't use my mouse. Instead, I just hit Enter on the keyboard. This also causes the "OK" button to "click", since it is marked as IsDefault="True". But guess what? The textbox doesn not lose focus in this case, and therefore, the binding engine remains innocently ignorant, and I don't get my data. Dang!

Another variation of the same scenario: suppose I have a data entry form right in the main window, enter some data into it, and then hit Ctrl+S for "Save". Guess what? My latest entry doesn't get saved!

This may be somewhat remedied by using UpdateSourceTrigger=PropertyChanged, but that is not always possible.

One obvious case would be the use of StringFormat with binding - the text keeps jumping back into "formatted" state as I'm trying to enter it.

And another case, which I have encountered myself, is when I have some time-consuming processing in the viewmodel's property setter, and I only want to perform it when the user is "done" entering text.

This seems like an eternal problem: I remember trying to solve it systematically from ages ago, ever since I've started working with interactive interfaces, but I've never quite succeeded. In the past, I always ended up using some sort of hacks - like, say, adding an "EnsureDataSaved" method to every "presenter" (as in "MVP") and calling it at "critical" points, or something like that...

But with all the cool technologies, as well as empty hype, of WPF, I expected they'd come up with some good solution.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172

6 Answers6

3

At critical points, you can force the binding to push through to your view model:

var textBox = Keyboard.FocusedElement as TextBox;
BindingOperations.GetBindingExpression(textBox, TextBox.TextProperty).UpdateSource();

Edit:

OK, since you don't want hacks we have to face the ugly truth:

  • In order to implement a clean view, the properties exposed by your view model should be friendly to frequent binding updates.

An analogy we can use is a text editor. If the application was a giant text box bound to a file on disk, every keystroke would result in writing the whole file. Even the concept of saving is not needed. That's perversely correct but terribly inefficient. We all immediately see that the view model needs to expose a buffer for the view to bind to and this re-introduces the concept of save and forces state handling in our view model.

Yet, we see this is still not efficient enough. For even medium-sized files the overhead of updating the whole-file buffer on every keystroke becomes unbearable. Next we expose commands in our view model to efficiently manipulate the buffer, never actually exchanging the whole buffer with the view.

So we conclude that in order to achieve efficiency and responsiveness with pure MVVM, we need to expose an efficient view model. That means that all text boxes can be bound through to properties with no ill effects. But, it also means that you have to push state down into the view model to handle that. And that's OK because a view model is not the model; it's job is it to handle the needs of the view.

It's true that we can rapidly prototype user interfaces by taking advantage of shortcuts like binding on focus changes. But binding on focus changes can have negative consequences in real applications and if so then we should simply not use it.

What is the alternative? Expose a property friendly to frequent updates. Call it the same thing as the old inefficient property was called. Implement your fast property using the slow property with logic that depends on the state of the view model. The view model gets the save command. It knows whether the fast property has been pushed through to the slow property. It can decide if when and where the slow property will be synched to the model.

But you say, haven't we just moved the hack from the view to the view model? No, we have lost some elegance and simplicity, but go back to the text editor analogy. We have to solve the problem, and it is the view model's job to solve it.

If we want to use pure MVVM and we want efficiency and responsiveness, then lame heuristics like let's avoid updating the binding source until the element loses focus won't help. They introduce as many problems as they solve. In that case, we should let the view model do its job, even if means adding complexity.

Assuming we accept it, how can we manage the complexity? We can implement a generic wrapper utility class to buffer the slow property and allow the view model to hook its get and set methods. Our utility class can auto-register for save command events to reduce the amount of boilerplate code in our view model.

If we do it right, then all the parts of the view model that were fast enough to be used with property changed binding will all still be the same, and the others that were worthy of asking the question "Is this property too slow?" will have a small amount of code to address the issue, and the view is none the wiser.

Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • This doesn't work on so many levels. Firstly, I don't want to use this kind of hacks, because it is not scalable or maintainable. For example, when I add another "critical point", I will have to remember all places where these hacks exist, and update them. Secondly, the view does not necessarily have the knowledge about "critical points". Their definition may be in another view or in viewmodel. And thirdly, this is just a lot of code to write and maintain. – Fyodor Soikin Jan 13 '11 at 07:39
  • Of course it's a hack! I was imagining you would do it in just one place: before you save. There can only be one text box that needs this treatment. A similar hack is to save the focused element, move it somewhere else, save, and then move it back. – Rick Sladkey Jan 13 '11 at 08:04
  • @Rick Sladkey, Well, then it was probably my fault, in that I was not clear enough. The point is, I want a clean solution. As far as dirty hacks go - I can come up with a million of them by myself. Just call me a "hacker" :-) – Fyodor Soikin Jan 13 '11 at 18:31
  • @Rick, regarding your edit: Thank you! This is exactly the kind of answer I was looking for. Your answer was efficient, in that it did inform me. I don't have the exact solution to particular little problems just yet, but it does feel like you've shown me the right way of looking at it. Indeed, it looks like I have grown too accustomed to MVVM and lost the sight of it's core idea. Thank you again. – Fyodor Soikin Jan 17 '11 at 15:47
  • Providing view details (which controls you use) to view model it is very bad. Because can be used many different views for same view model. Second problem is that in some view can be used unique control, not TextBox. Yes, I agree that VM encapsulate logic of view, but common part of all similar views that can be created for this VM. – SeeSharp Jan 17 '11 at 17:28
  • @SeeSharp: Perhaps I could have been more clear. The view model only exposes a string property and commands, ala pure MVVM. As you are saying, it should be UI neutral. The view binds to that string and issues commands, e.g. Apply, Revert, Refresh, Save, whatever is appropriate. – Rick Sladkey Jan 18 '11 at 00:37
  • And where should be used code to move focus? I think it is view level problem and solution should be appropriate. See my post below “What you think about proxy command and KeyBinding to ENTER key?”. – SeeSharp Jan 18 '11 at 06:53
0

This is a tricky one and I agree a non-hack and more-or-less code free solution should be found. Here are my thoughts:

  1. The view is responsible because it sets IsDefault to true and allows for this 'problem'
  2. The ViewModel should not be responsible in any way to fix this it might introduce dependencies from VM to V and thus breaking the pattern.
  3. Without adding (C#) code to the View all you can do is either change bindings (e.g. to UpdateSourceTrigger=PropertyChanged) or add code to a base class of the Button. In the base class of the button you might be able to shift focus to the button before executing the command. Still hack-ish but cleaner than adding code to the VM.

So at the moment the only 'nice' solutions I see require the view developers to stick to a rule; set the binding in a specific way or use a special button.

Emond
  • 50,210
  • 11
  • 84
  • 115
  • And what about the case with pressing "Ctrl+S"? – Fyodor Soikin Jan 15 '11 at 16:04
  • Changing the binding would solve that I think. Overriding the button wouldn't solve it. – Emond Jan 15 '11 at 18:03
  • In the original post, I provided two possible reasons (both of which exist in my current project) that make impossible the use of `UpdateSourceTrigger=PropertyChanged` – Fyodor Soikin Jan 15 '11 at 18:22
  • 1
    If you only want to write to the VM when all input is entered you need to revert to UpdateSourceTrigger=Explicit and, indeed, handle the event by writing code. BTW: if it takes a property that long to be set, I suspect the property setter ought to be a function. Sorry, I don't think this is possible in any other way. – Emond Jan 15 '11 at 19:07
  • do you have significant experience designing user interfaces? If so, how did you usually go about solving this problem? Did you really always invent a one-time hack for every situation? I'm primarily waiting for some answers from GUI professionals, who deal with these things on a daily basis, and have some well-developed way of dealing with it. – Fyodor Soikin Jan 15 '11 at 19:18
0

The problem is that the TextBox's text has a default source trigger of LostFocus instead of PropertyChanged. IMHO this was a wrong default choice since it is quite unexpected and can cause all sorts of problems (such as the ones you describe).

  1. The simplest solution would be to always explicitly use UpdateSourceTrigger=PropertyChanged (as the others suggested).
  2. If this isn't feasible (for whatever reason), I would handle the Unloaded, Closing or Closed events and manually update the binding (as shown by Rick).

Unfortunately it seems that certain scenarios are still a bit problematic with a TextBox, so some workarounds are necessary. For example, see my question. You might want to open a Connect bug (or two) with your specific problems.

EDIT: Pressing Ctrl+S with focus on the TextBox, I would say the behavior is correct. After all, you are executing a command. This has nothing to do with the current (keyboard) focus. The command may even depend on the focused element! You are not clicking on a button or similar, which would cause the focus to change (however, depending on the button, it may fire the same command as before).

So if you want to only update the bound Text when you lose focus from the TextBox, but at the same time you want to fire a command with the newest contents of TextBox (i.e. the changes without it having lost focus), this does not match up. So either you have to change your binding to PropertyChanged, or manually update the binding.

EDIT #2: As for your two cases why you cannot always use PropertyChanged:

  1. What precisely are you doing with StringFormat? In all my UI work so far I use StringFormat to reformat data I am getting from the ViewModel. However, I am not sure how using StringFormat with data which is then again edited by the user should work. I am guessing you want to format the text for display, and then "unformat" the text the user enters for further processing in your ViewModel. From your description, it seems it isn't "unformatted" correctly all the time.
    1. Open a Connect bug where it isn't working as it should.
    2. Write your own ValueConverter which you use in the binding.
    3. Have a separate property with the last "valid" value and use that value in your ViewModel; only update it once you get another "valid" value from the property you use in databinding.
  2. If you have a long-running property setter (ex. a "validate" step), I would do the long-running part in a separate method (getters and setters should normally be relatively "fast"). Then run that method in a worker thread/threadpool/BackgroundWorker (make it abortable so you can restart it with a new value once the user enters more data) or similar.
Community
  • 1
  • 1
Daniel Rose
  • 17,233
  • 9
  • 65
  • 88
  • I wonder what I would write in the bug. This seems kind of like by design. It's just that the design is flawed, but I can't really say that something is malfunctioning... – Fyodor Soikin Jan 15 '11 at 16:04
  • @Fyodor: Bug: When closing a form via Enter key (did you also check canceling via Escape?), the LostFocus-binding on a TextBox is never fired. For the second part, see my edit. – Daniel Rose Jan 15 '11 at 19:34
  • Regarding your "EDIT": that is kind of the whole point of my question! I DO understand that there is no "technical" reason for the source to be updated when I press Ctrl+S. On the other hand, this behavior is what the user would expect, isn't it? And that is precisely the essence of my question: how does one go about solving this problem in WPF? And I'm not just looking for a one-time hack-style solution. I'm looking for the general approach, for a design pattern, if you will. – Fyodor Soikin Jan 16 '11 at 09:16
  • @Fyodor: There are currently three possibilities when a binding is updated: PropertyChanged, LostFocus, Explicit. These three work as advertised and IMHO what the "user" (or rather developer) expects. What you want is a fourth possibility: LostFocusOrCommandExecuting. Unfortunately, this does not exist. So either you have to do it yourself (by keeping it at LostFocus and manually updating the binding when the command fires) or you switch to PropertyChanged. – Daniel Rose Jan 16 '11 at 13:33
  • @Fyodor See my edit as to why you cannot use PropertyChanged. – Daniel Rose Jan 16 '11 at 21:14
  • Regarding your EDIT#2. First. I am using StringFormat to, say, format a floating-point number to have 2 digits after the point. This works really nicely with LostFocus: you type your number, hit TAB, and - boom! - the number automatically gets a nice decimal point with two zeroes after it. But with PropertyChanged, this turns ugly: the point with two zeroes keep appearing after each keystroke, which is very annoying. Another example would be formatting phone numbers to have all the spaces and dashes in right places. You know, that kind of things. – Fyodor Soikin Jan 17 '11 at 15:15
  • Second. The setter is not that "long". It takes about, like, 80-100ms, tops. And it does, indeed, at some point, kick off some background processing. But nevertheless, it turns out to be way too much if it happens on every keystroke, given the average speed of a person typing. And even if not for that, I still would prefer not to do this processing more than really needed, because it does use up some other resources, besides CPU cycles (like, say, internal object IDs, so that the ID sequence comes out with ugly holes in the end). – Fyodor Soikin Jan 17 '11 at 15:20
  • And then again, I must repeat myself: please do not try to solve these particular problems. Firstly, I only gave them as examples, and secondly, the original question is not about them. Because - YES, I DO KNOW HOW TO SOLVE PARTICULAR LITTLE PROBLEMS. Believe it or not, I'm actually a pretty good engineer, as well as coder. I can read documentation, I can use .NET Reflector, I can do my own research, et cetera, et cetera. What I came here for is the one thing I can't get by brute force - the experience of smart people. – Fyodor Soikin Jan 17 '11 at 15:27
  • @Fyodor Well what do you want? The command system and bindings generally work as designed and as it should given the "logic" behind WPF. However, you have a different idea of how they should work. That's fine, however this means you have to do some work to make it work "non-standardly". It won't work by magic (see my comment above on updating bindings; you want something which is contrary to how commands are designed in WPF). But when people give possible solutions, all you say is "no". BTW all you gave so far are "little problems" which people gave solutions for. What big problem is there? – Daniel Rose Jan 17 '11 at 15:50
  • See the edit from @Rick Sladkey that I've marked as answer. This is what I was looking for. This is probably my fault that you don't see the real conceptual problem behind all the small ones, but this is the best I can do as far as explaining. Rick, for one, was able to get what I mean exactly. – Fyodor Soikin Jan 17 '11 at 16:09
  • @Fyodor Well, I for one don't see much of a difference (in the solution provided) between my answer (Edit #2) and Rick's answer, besides that I've written less text and didn't explicitly state that you can then possibly move the additional logic required to an additional "wrapper utility" class. – Daniel Rose Jan 18 '11 at 07:38
  • @Daniel: If you don't see the difference, then this question was probably not for you in the first place. Sorry for your wasted time. – Fyodor Soikin Jan 18 '11 at 07:53
0

I would add a Click event handler for the default button. The button's event handler is executed prior the command will be called, so the data bindings can be updated by changing the focus in the event handler.

private void Button_Click(object sender, RoutedEventArgs e) {
    ((Control)sender).Focus();
}

However, I don't know if similar approach can be used with other shorcut keys.

mgronber
  • 3,399
  • 16
  • 20
  • Again, a one-time hack. Not maintainable, not scalable, not intuitive. And doesn't address the other scenario - the one with pressing "Ctrl+S". – Fyodor Soikin Jan 15 '11 at 15:56
0

Yes, I have quite some experience. WPF and Silverlight still have their pain areas. MVVM doesn't solve it all; it is not a magic bullet and the support in the frameworks is getting better but still lacks. E.g., I still find editing deep child collections a problem.

At the moment I handle these situations case by case because a lot depends on the way the individual view have to work. This is what I spend most of my time on because I generate a lot of plumbing using T4 so I have time left for these quirks.

Emond
  • 50,210
  • 11
  • 84
  • 115
0

What you think about proxy command and KeyBinding to ENTER key?

EDITED: There we have one utility command (like converter), which requires knowledge about concrete view. This command can be reused for any dialog with same bug. And you add this functionality/hack only in view where this bug exists, and VM will be clear.

VM creates to adapt business to view and must provide some specific functionality like data conversion, UI commands, additional/helper fields, notifications and hacks/workarounds. And if we have leaks between levels in MVVM we have problems with: high connectivity, code reuse, unit testing for VM, pain code.

Usage in xaml (no IsDefault on Button):

<Window.Resources>
    <model:ButtonProxyCommand x:Key="proxyCommand"/>
</Window.Resources>

<Window.InputBindings>
    <KeyBinding Key="Enter"
          Command="{Binding Source={StaticResource proxyCommand}, Path=Instance}" 
          CommandParameter="{Binding ElementName=_okBtn}"/>
</Window.InputBindings>
<StackPanel>
    <TextBox>
        <TextBox.Text>
            <Binding Path="Text"></Binding>
        </TextBox.Text>
    </TextBox>
    <Button Name="_okBtn" Command="{Binding Command}">Ok</Button>
</StackPanel>

There used special proxy command, which receive element (CommandParameter) to move focus and execute. But this class requires ButtonBase for CommandParameter:

public class ButtonProxyCommand : ICommand
{
    public bool CanExecute(object parameter)
    {
        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return false;

        return btn.Command.CanExecute(btn.CommandParameter);
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        if (parameter == null)
            return;

        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return;

        Action a = () => btn.Focus();
        var op = Dispatcher.CurrentDispatcher.BeginInvoke(a);

        op.Wait();
        btn.Command.Execute(btn.CommandParameter);
    }

    private static ButtonProxyCommand _instance = null;
    public static ButtonProxyCommand Instance
    {
        get
        {
            if (_instance == null)
                _instance = new ButtonProxyCommand();

            return _instance;
        }
    }
}

This is just idea, not complete solution.

SeeSharp
  • 1,730
  • 11
  • 31
  • So, let me make sure I got this correctly. Your idea is to use a proxy command that will shift focus, and then delegate to another command. And use this kind of proxy command everywhere where I might possibly care about getting the latest data. Right? – Fyodor Soikin Jan 17 '11 at 15:37
  • Yes. And this work. I think it is correct to use additional command for additional functionality (Enter key) and redirect it to original control and command. Note: KeyBinding and ButtonProxyCommand only one on whole form. Proxy used for button, not for each editor control (TextBox, NumericUpDown, etc.). – SeeSharp Jan 17 '11 at 16:21