Oh boy, this was an interesting one…
Here at the company we have our application framework, and we also have a little framework that sits on top of the DevExpress DXperience controls suite. We have an edit form that has the typical CRUD buttons on the Ribbon. Those buttons get enabled/disabled depending on the context (if the data is "dirty", both the Save and Undo buttons become enabled; otherwise, they’re disabled):
Now, say the user launches an edit form, puts the cursor in a TextBox, types something in, and clicks the Save button. Clicking directly on the button on the Ribbon does not cause the TextBox to lose its focus, and because of the way databinding works in .NET, the changes made to the TextBox never get pushed into the underlying object the control is bound to (most likely, a property in a Business Entity).
Just for the records, databinding doesn’t update the underlying value immediately for performance reasons (changing the underlying property may cause a series of changes, and we don’t want that to happen every time the user hits a keystroke within a TextBox).
We created a method, ForceDataBindingToSource, that grabs the ActiveControl, and pulls the data out of it into the underlying business entity’s property. Since the Save button on the Ribbon calls a SaveData method on the form, we just made SaveData call ForceDataBindingToSource, and then the issue (tab off required so to kick databinding) was solved; or almost.
The TextBox that we use in this application is a subclass of DevExpress TextBox class. We’ve made this subclass implement one of our interfaces, so make it work well with our homegrown databiding mechanism. I noticed that the code in ForceDataBindingToSource wasn’t working on the DX TextBox
Debugging the code I noticed that the ActiveControl was of type TextBoxMaskBox (which does not implement our required interface), even though the control I have on the form is really a subclass of the TextBox class. That puzzled me. I then used the Runtime Object Editor tool to inspect my UI controls during runtime.
The TextBox looked like the one I really had on the form. With a little further inspection, I can actually get to see that there really is a MaskTextBox control within the TextBox:
OK, after having figured that out, I was able to fix my code so to make sure I looked into ActiveControl.Parent (which in the case of a MaskTextBox should be the parent TextBox), and there I can find what I’m looking for.
So that fixes the problem of the user having to tab off the field in order to cause databinding to kick in.
But wait, there’s more. Remember I said I need to synchronize the Ribbon buttons depending on the context? Our business entity raises an event as soon as anything changes on it, so we hooked up into that event to synchronize the Ribbon accordingly. But, again, if the user launches the form and type something into a TextBox, the entity does not get updated until the TextBox loses focus. So, at this point, the user can’t even click on the Save button, which would cause the entity to refresh, since the button is disabled. Chicken and the Egg kind of scenario.
One could say, "well, but that’s easy: just hook up to the TextChanged event and you’re all set!". That is true for TextBoxes, but what about the other controls? I mean, a TextBox has the TextChanged event, a CheckBox has a CheckStateChanged event, a ComboBox has a SelectedIndexChanged event, and so on and so forth. How can I make that generic, then?
Fortunately, in order to implement another feature of this application, I had faced this very same issue (needed a generic event raised whenever any control had its content changed), and solved that by having the classes implement an interface and route the specific events (TextChanged, SelectedIndexChanged, etc.) to a single event defined by the interface.
With that in mind, I was able to hook up that generic event to a single event handler, which calls a FlagAsDirty method on my form, causing the form to go into "dirty mode".
I’m just glad to have found a solution that works in a generic way, and the developers using these classes don’t have to worry about all these nasty issues.