tags:

views:

297

answers:

1

I'm writing an undo journal for my WPF data-entry screen, which will track changes across all the controls. When the user selects Undo, I want to not only revert the latest change, but put focus back in the control whose value is being reverted. I'm struggling with the best way to put that focus back.

My ViewModel will be the part that handles the undo journaling: the ViewModel's property setters will capture some "before" state before updating the DataModel. One way or another, that "before" state needs to include enough information for me to be able to put the focus back later.

For illustration, let's say there are two data-entry fields: Address and City. The ViewModel has a property for each, and the View has a TextBox for each that's bound to the corresponding ViewModel property.

Let's follow the example where the user has just typed a value into the Address field, and then clicked on the City field. I'm using the default UpdateSourceTrigger.LostFocus behavior, so the Address change gets saved when the Address TextBox loses focus. I've got three different ideas so far on how to approach this, but I don't know enough details about WPF to know how to make any of them work.

  1. I could forget about MVVM-style databinding, and hook the edit controls' LostFocus events (or add an attached behavior, or make a custom control that wraps a TextBox, or...). In the LostFocus event handler, I could create an undo frame that includes a reference to the event's sender. Later, after Undo, I just focus the control whose reference I saved. This is probably what I would have done in WinForms, but in WPF I'd rather stick with the ViewModel pattern -- I'd rather have the journaling logic live in the ViewModel than the View, for testability if nothing else. So this option isn't my first choice.

  2. In my ViewModel's property setters, I could capture the name of the ViewModel property that's being set ("Address" in this example), and store that name in the undo frame. Later, on Undo, I could iterate through all the controls in the View, looking for the first one I can find that has something bound to a property named Address. As soon as I find one such control, I give it focus. This would be good enough for what I need, since I don't expect to have more than one control bound to the same ViewModel property. The problem is that this would require digging into binding expressions, which is something I don't know how to do. (It would also introduce more name-based late binding that could break if I refactor.)

  3. When my ViewModel adds the change to the undo stack, it could ask the View layer (via an interface) to create a Memento that knows which control has focus. On Undo, the journal would ask the View to restore that Memento. The problem here is that, by the time my ViewModel's property is getting set and I'm adding the undo frame, the keyboard focus has already moved to the City TextBox, so the "create memento" would need to be trickier than just "where's the current keyboard focus right now", and I'm not sure how to accomplish that trick.

Anyone have any suggestions on making any of the above work, or have alternate approaches that might work better?

+1  A: 

I would start with your second approach. However, instead of digging through the binding list, I would hard code the control's highlight property to a VM property.

For example, this is my VM:

public class VM
{
    public double Price { get; set; }
    public bool PriceHighlighted { get; set; }
}

Then, bind Price property to a TextBox and the TextBox's background to PriceHighlighted (with a value converter). Now VM has the total control of how the view should react. When the user does "Undo", VM can set all xxxHightlighted to false except the one you want to highlight.

Kai Wang
Interesting idea. Doesn't answer the question of how to scroll that field into view, but it would certainly be testable.
Joe White
How to scroll the view depends on how the view can consume the infomation the VM provides. For example, if all the textboxes are in a collection bound to a listview, you can scroll the view by set the selectedIndex of the collectionView.
Kai Wang