The pattern I favor in many cases to have private fields which indicate the logical state of controls, and properties which cause the properties of the controls themselves to be updated in a reasonably-timely fashion. If there's one point of interaction with the properties of the controls themselves, and provided that the fields always represent a valid state, then non-UI-thread code can change the state of the controls without having to synchronize itself with the UI thread; it merely has to ensure that there's a request pending to update the controls when next convenient.
Although your particular example doesn't involve any threading issues, and thus doesn't really benefit from the ease of threading afforded by separating out control state from logical state, using the same pattern for cases requiring multi-threading support as for cases that don't will reduce the number of different patterns one has to deal with. Additionally, if you use your own fields to represent the different states the controls can have, you can ensure that they will never represent an ambiguous or invalid state. For example, if a piece of code set the button to "UNDO" rather than "Undo", some other code might decide that "it isn't 'Undo', so it should save", some might decide that "it isn't Save
, so it should undo"`, some might do nothing, and some might squawk. Further, it may be difficult to add any additional states or indications. For example, one might want to add to the "Undo" button an indication of what would be undone. If one uses fields to track states, one may be able to add such functionality without disturbing the field that distinguishes between "save" and "undo"; either add another field to keep track of what specific operation should be indicated when the undo button is shown, or else have the "update button text" routine use existing fields to determine that.