none
User to confirm saving / discarding changes. RRS feed

  • Question

  • I have a form with individual textboxes bound to fields in a cursor, as well as a grid with the records in the cursor.

    I would like a "save" button to change color the moment a user changes any of the values in any of the text boxes and/or the grid.

    Any simple way to do this? 

    Thanks!

    Wednesday, August 22, 2018 1:11 AM

Answers

  • Hi Aleniko2,

    the common way of doing this would be to use your own textbox class that offers this feature as an option that can be activated if needed.

    However, if this isn't an option you can do this i.e. with BINDEVENT() like this:

    * // Code in your FORM.INIT method
    PUBLIC myForm as Form 
    myForm = Thisform 
    FOR EACH loObject IN Thisform.Objects 
    	IF loObject.BaseClass = [Textbox]
    		BINDEVENT( loObject , [InteractiveChange] , myForm , [ModifiedHandler] , 1 )	
    	ENDIF 
    ENDFOR 
    
    * // Code in the new Foorm method "ModifiedHandler"
    LPARAMETERS vDummy as Variant
    Thisform.ActiveControl.BorderColor = RGB(255,0,0)

    This code changes the BorderColor of each textbox as soon as it was edited.

    When doing this with a button, all you have to change is that you can change the color already in the FOR EACH...ENDFOR loop instead of binding the object to a method.

    ...and activating the code via Button.Click...

    HTH


    Gruss / Best regards
    -Tom
    Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible,
    you are, by definition, not smart enough to debug it. 010101100100011001010000011110000101001001101111011000110110101101110011




    Wednesday, August 22, 2018 6:29 AM
    Moderator

All replies

  • Hi Aleniko2,

    the common way of doing this would be to use your own textbox class that offers this feature as an option that can be activated if needed.

    However, if this isn't an option you can do this i.e. with BINDEVENT() like this:

    * // Code in your FORM.INIT method
    PUBLIC myForm as Form 
    myForm = Thisform 
    FOR EACH loObject IN Thisform.Objects 
    	IF loObject.BaseClass = [Textbox]
    		BINDEVENT( loObject , [InteractiveChange] , myForm , [ModifiedHandler] , 1 )	
    	ENDIF 
    ENDFOR 
    
    * // Code in the new Foorm method "ModifiedHandler"
    LPARAMETERS vDummy as Variant
    Thisform.ActiveControl.BorderColor = RGB(255,0,0)

    This code changes the BorderColor of each textbox as soon as it was edited.

    When doing this with a button, all you have to change is that you can change the color already in the FOR EACH...ENDFOR loop instead of binding the object to a method.

    ...and activating the code via Button.Click...

    HTH


    Gruss / Best regards
    -Tom
    Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible,
    you are, by definition, not smart enough to debug it. 010101100100011001010000011110000101001001101111011000110110101101110011




    Wednesday, August 22, 2018 6:29 AM
    Moderator
  • If you're working with tablebuffering you can detect whether changes in a workarea exist with two ways:

    1. For the current record with GetFldState(-1), giving you a string of all field states without needing to adress every single field. If any digits is 2 or 4 you have a change. 3 means an unchanged new record, in buffered mode that also means a change compared with the DBF, as this new record isn't yet committed, so anything other than 1 indicates the need to save.

    2. For the whole workarea GetNextModified(0) should return any record number other than 0 to indicate there is a change.

    Notice, this will aslo depend on workarea/alias name. So if your form maintains several workareas, check all of them, there is no indicator on the level of all workareas in a datasession like GETNEXTMODIFIEDWORKAREA or similar.

    Tom's approach of binding to the interactivechange event is valid and working, but it would be simpler to have this inside any base controls. I would let both interactivechange and programmaticchange call a central new control method that simply sets the bordercolor or backgroundcolor or change the save button color or picture (watch out: Themes can hinder such color changes to be seen especially for the standard color of buttons or tabs or other such things in the typical form color). Programmaticchange can also simply mean the binding, so before you indicate a change check out GETNEXTMODIFIED(0)<>0 or you may indicate a change that isn't yet existing.

    By the way don't be surprised if you change a text and tehn change it back to the original value, that's still seen as a modification of the field. VFP is not reverting the fieldstatus, if its value is equal to OLDVAL(), but you could in this same method, especially not in a cnetralized form method but in a per control method having easy access to This.Controlsource to know which fieldstate to check without the need of a method called by bindevent to find out the source of the event with AEVENT. You can set the fieldstate with SetFldState().

    For the benefit of better maintainability and extensibility I'd recommend going with base controls instead of eventbinding. Many years later it's less obvious why things happen, if they happen by bindevents.

    Bye, Olaf.


    • Edited by OlafDoschke Sunday, October 7, 2018 9:15 AM
    Wednesday, August 22, 2018 7:23 AM
  • watch out: Themes can hinder such color changes to be seen especially for the standard color of buttons or tabs or other such things in the typical form color

    Exactly!

    I forgot to mention this in my first post. The color change will only happen if FORM.Textbox.Themes is set to .F.


    Gruss / Best regards
    -Tom
    Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible,
    you are, by definition, not smart enough to debug it. 010101100100011001010000011110000101001001101111011000110110101101110011

    Wednesday, August 22, 2018 10:19 AM
    Moderator
  • That's right for BorderColor, the SpecialEffect property also has to be 1 -Plain, otherwise you get a 3D border not changing color, even though Themes=.F.

    But you could set the backcolor of all controls, I was wrong with the commandbutton backcolor, thought that is in the typical themes form color, but it blends with the color you set, so you get a mixture - that is at least happening on Windows 10.

    The OS Version might also play a role, eg in Vista the command button backcolor might not change with Themes=.T.

    In the end I'd rather set things working under any circumstances, like the Button Picture or you could Enable the button and disable it before there are changes to save. 

    The backcolors of any editing controls (aside buttons) that have any white part, even options or checkboxes will work and display a new color with any themes and specialeffect styling - though I didn't test all possible combinations.

    I also played a bit with the automatically reverting style, also indicating when you reverted all changes. It's less important, as you can simply add a cancel button doing Tablerevert(.f.) and in the end it turns out you need to do that anyway, because even after resetting all single field states with SetFldState() and GetFldState(-1) returns just "1" or just "3" digits to indicate an unmodified record, GetNextModified(0) does not return 0. And there are other pitfalls like Valid setting fieldstates back, etc. Not really worth the hassle, since you can also reset the looks and state with Tablerevert(.f.) anyway. So @Aleniko: When you simply indicate a change at any interactive change that's good enough as indication. When it comes to saving I'd check what there is to save with getnextmodified(0)<>0, though. And I mean <>, as recno() of new records are negative, only 0 indicates no changes at all in any old or new records. It does not hurt to do  Tableupdate() even if the buffer is empty, but you might go through some rule checking code that's not necessary, if there are no changes to save.

    Bye, Olaf.

    • Edited by OlafDoschke Wednesday, August 22, 2018 1:50 PM
    Wednesday, August 22, 2018 12:05 PM
  • Thank you both for the insights. I'm going to go for the bindevent method for this specific situation.


    Wednesday, August 22, 2018 7:20 PM
  • Tom;

    I have a few questions about what you wrote:

    1. I didn't understand where the modifiedhandler goes. Can you elaborate on that?

    2. If I want a "save" button to change color rather than the textboxes - how would going through a loop in the init method of the form do that?

    3. I did consider putting the functionality into my main textbox class - do you think its a better practice?

    Thank you so much for your help.

    Wednesday, August 22, 2018 7:34 PM
  • 1. Add the method to your form. Forms are the only VFP-Class where you can add new properties and methods at designtime without creating a subclass first.

    2. There are several ways to achieve this:

    2.a Code the loop directly in your button (baaaad)

    2.b Calling a method from the button (better)

    2.c Bind the buttons click event in the forms init to the method that does the job (better)

    2.d Subclass your main button class and create a specialized buttonclass for doing it (good)

    3. It's an option, but IMHO, not the best way because some time in the future you'll have a main textbox class with so many functionality that would better be placed in specail subclasses. See 2.d ;)

    Now, some additional infos to 1.

    Looking at my code snippet you can see that I declared a public variable that holds a reference to the form. This is a quick and dirty way to do this. A better way could be to create a references object within _SCREEN that holds all references that you need in i.e. other forms. That way you have a central point where you cann kill all references that might hinder a clean close of your app. When using a public var you will have to take care to release it on form.unload like this: myForm = .NULL. -and- Release myForm!!! (a little 2-liner)

    OK, back to the public var 'mForm'. This is needed because BINDEVENT doesn't work with THISFORM. BINDEVENT needs a form reference thats accessible from outside the form, so to say. Therefore a _screen.myReferences collection comes in handy.

    Now, when looking at the BINDEVENT() params, you will understand why there isn't 'Thisform' but 'myForm' as target.

    HTH


    Gruss / Best regards
    -Tom
    Debugging is twice as hard as writing the code in the first place.
    Therefore, if you write the code as cleverly as possible,
    you are, by definition, not smart enough to debug it. 010101100100011001010000011110000101001001101111011000110110101101110011

    Wednesday, August 22, 2018 7:57 PM
    Moderator
  • FWIW, I cover this topic in my BindEvents paper: http://tomorrowssolutionsllc.com/ConferenceSessions/Bind%20Events%20for%20Better%20Applications.PDF. Look for the section "Tracking User Changes"

    Tamar

    Wednesday, August 22, 2018 8:40 PM
    Moderator
  • This is very helpful.

    Regarding #2: Maybe I don't understand you (Or you me...) but I don't see how coding anything into the button can affect its color when a textbox gets changed - unless I use the refresh method of the button, AND I refresh at the valid of every textbox... Am I missing something?

    Thanks!


    • Edited by Aleniko2 Wednesday, August 22, 2018 10:27 PM
    Wednesday, August 22, 2018 10:26 PM
  • Re question 2:

    All you need to change to act on the cmdSave button backcolor instead of the textbox border colors is

    replacing this line:

    Thisform.ActiveControl.BorderColor = RGB(255,0,0)

    With this one:

    Thisform.cmdSave.BackColor = RGB(255,0,0)

    And name your save button cmdSave, of course, or change above line to reflect your command button name

    The event triggering the code still is all controls interacivechange events, thereofre the loop for each object in thisform.objects. That Objects collection simply is all form controls, no matter what's on the form all of them are bound to the single method doing whatever you want.

    Bye, Olaf.

    • Edited by OlafDoschke Thursday, August 23, 2018 10:07 AM
    Wednesday, August 22, 2018 11:17 PM
  • The argument to not create a "god class" that can do anything and is loaded with way too much functionality is ok, I can support that.

    I still dislike the thought to need bindevents, espceially an unpredictable number of them. I see it in terms of encapsulaition you are faced with an unsolvable problem of concentrating all code into any single element. One problem none of use have given a thought is about a form that extends at runtime, it's not that weird to have a pageframe loading pages, if only init code binds all controls existing at start of the form those loaded later would not influence the save button and unsaved status.

    So I'd argue to do this a bit from all sides. The one central element could be a sepcialised form class, which can have quite a lot even more specialised child form classes, but would be the edit form class. It might always have a cmdSave button. Then you already have a larger encapsulation. It would still not include all other contrls and it makes no sense to do that, as that wouldn't result in a single base class but in code that's individual in every form.

    You still can let each control have the interactivechange function modified in it's base class, considering what we're doing here is a very small amount of code just signalling to anything else "there was a change here". Indeed let's neither target a save button nor the control itself, lets just use a form property .lUnsavedChanges. You can clearly do this one line action without fearing this will burden you with a load and cause any later control class to work sluggish. The only dependency you would have is a form needs such a property. You can even be cautious about that and let the code check whether such a property exists before setting it, you could simply create the proeprty with Addproperty and you could also just put Thisform.lUnsavedChanges= .T. in a TRY..CATCH block. Anyway, this part is simply done now and whoever is interested in such a flag can read it now.

    The next could be definig a lUnsavedChanges_assign Method, that is a method, that automatically runs when any code sets this property. It can react to the change and for example set the save button color. That's the classic way of doing something similar to Bindevents, but simpler to detect, as it's actively done from the event source to a property, which then affects a target, perhaps.

    But you could also extend that without an ssign method from the save button side. It now can use Bindevents, but instead of binding to all controls, it would just bind to the form.lUnsavedChanges property. And again this binding could create the property to be able to bind to it, or do the binding in a TRY..CATCH. So only when all ingredients come together, a form with such a property, controls setting it and a save button binding to it, you have the overall wanted functionality, if only two parts come together you still can have any other usage of this, also a save button of a toolbar could be programmed to bind to the lUnsavedChanges property of any form that's started when a form handler manages that at form start.

    This way you have building blocks, that can work together but are still independent and decoupled from each other when not coming together.

    So in short the ingredients would be:

    A Form implementing an lUnsavedChanges property, initially .F.

    A Textbox or any other base control doing this in InteractiveChange:

    Try
       Thisform.lUnsavedChanges =.T.
    Catch
       *
    Endtry

    A save button doing this in Init:

    Try
       BindEvent(Thisform,"lUnsavedChanges",This,"IndicateChanges")
    Catch
       *
    Endtry

    And an IndicateChanges method of the same save button class just doing:
    This.BackColor = RGB(255,0,0)

    This can easily be extended. When you have a form handler keeping a collection of running forms in a VFP collection, this could use this information in a cleanup Quit code to autosave changes on a "management level", though I like to let every form handle that in itself in QueryUnload.

    Since nothing is prerequisite in this case, you can addd controls at runtime, you even could add the save button at a later time and it'd still work, it'd just perhaps also call IndicateChanges() from init when the lUnsavedChanged property already is .T.

    And you could easily add the lUnsavedChanges property to forms not based on EditForm, too. So you also have a way out of the problematic situation your form is based on the wrong branch of classes or the form mainly is another type of form than an edit form.

    Bye, Olaf.

    • Edited by OlafDoschke Monday, August 27, 2018 2:00 PM
    Thursday, August 23, 2018 8:17 PM