Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Avoiding EN_CHANGE notifications

0.00/5 (No votes)
30 Jun 2008 1  
Handling complex control interactions when edit controls are involved can lead to problems when EN_CHANGE notifications are generated by the application actually changing the control values. To avoid having to handle EN_CHANGE notifications from CEdit and CRichEdit, this article shows how to derive

Introduction

One of the problems that is intrinsic to Windows is the fact that an ordinary Edit control, MFC class CEdit, will generate EN_CHANGE notifications any time the text changes, even if the text is changed programmatically by the application.

This can lead to awkward situations; for example, if two edit controls have some invariant which must be maintained, responding to an EN_CHANGE event in one may necessitate changing the text of the other; but, since a change in the other requires a change in the first, setting that text triggers another EN_CHANGE, during which the handler for the second control changes the contents of the first control, which triggers an EN_CHANGE which causes the application to then respond by changing the second, and so on, until there is a stack overflow due to the infinite recursion that goes on.

In this article, I will show how to avoid EN_CHANGE notifications in ordinary CEdit controls, and also how much simpler it is to implement the same functionality in a Rich Edit Control.

Avoiding it in CEdit

To avoid EN_CHANGE notifications in a CEdit control, you must create a subclass of the control. In my case, it was called CNoNotifyEdit.

To this subclass, and a "Reflected event handler" for EN_CHANGE, which is indicated by the =EN_CHANGE notification:

Next, hand-edit the resulting header file and implementation file.

In the Message Map, change the line that says:

ON_CONTROL_REFLECT(EN_CHANGE, OnEnChange)

to read:

ON_CONTROL_REFLECT_EX(EN_CHANGE, OnEnChange)

This requires a change in the prototype for the method, so change:

public:
afx_msg void OnEnChange();

to:

protected:
afx_msg BOOL OnEnChange();

Note that I not only change the result type from void to BOOL, but I also change the scope to be protected. There is not now, and never has been in the entire history of MFC, a sensible reason to make these methods public, since any program that ever attempted to call them from outside the class would represent such an egregious breach of sensible OO methodology that no one would ever do it. I always change my handlers to be protected. (This represents one of the numerous blunders of Visual Studio .NET, which has been more of an exercise in destroying the usability of the IDE than of fixing ridiculous bugs and design flaws.)

Then, I add a new method:

public:
void SetWindowTextNoNotify(LPCTSTR s);

and add a variable:

protected:
BOOL notify;

Initialize the notify variable in the constructor:

CNoNotifyEdit::CNoNotifyEdit()
{
    notify = TRUE;
}

The implementation of SetWindowTextNoNotify is:

void CNoNotifyEdit::SetWindowTextNoNotify(LPCTSTR s)
{ 
     CString old;
     CEdit::GetWindowText(old);
     if(old == s)
        return; // do nothing, already set
     BOOL previous = notify;
     notify = FALSE;
     CEdit::SetWindowText(s);
     notify = previous;
}

As an optimization, I simply check to see if the existing string is the same as the string I'm about to set, and if they are, I do nothing. If you had edit controls with massive amounts of text, you might choose to eliminate this step because of its impact on memory fragmentation.

Finally, the implementation of the reflected EN_CHANGE handler is quite simple:

BOOL CNoNotifyEdit::OnChange() 
{
   return !notify;
}

The way a reflected handler works is that if the handler returns TRUE, it means the handler has done everything necessary to handle the event and it will not be sent to the parent window. So, in this case, if notify is FALSE (no notification desired), the handler returns TRUE. But, if notify is TRUE, then the handler returns FALSE, which MFC interprets as "Please pass this event to the parent window as usual".

Avoiding it in CRichEditCtrl

The problem is a little different in a Rich Edit control. First, Rich Edit controls do not normally generate EN_CHANGE notifications. Whenever you subclass an EN_CHANGE handler in a rich edit control, you get comments that say:

    // TODO:  If this is a RICHEDIT control, the control will not
    // send this notification unless you override the CEdit::OnInitDialog()
    // function and call CRichEditCtrl().SetEventMask()
    // with the ENM_CHANGE flag ORed into the mask.

Now, this message is more than a little strange. For example, the CEdit class would not be involved at all. It would be the CRichEditCtrl class. And, neither of these have an OnInitDialog handler, because that is only a member of the CDialog class. You would not call CRichEditCtrl().SetEventMask() because it would make no sense to apply the SetEventMask call to a constructor! It is not clear why you would get this for a CEdit control at all, since the ClassWizard knows the class of the control, and therefore would not need to put these comments in except for classes derived from CRichEdit. It also does not explain what is meant by "ORed into the mask", since what is required to OR something in is to have a value in the first place into which to OR it. So, other than the fact that the comment has approximately one deep and fundamental bug per line, it makes perfect sense. Not.

What does this comment really mean?

Well, if the control is in a CDialog-derived class, including dialog bars and property pages, you would typically enable the events as part of the OnInitDialog handler. If it is part of a CFormView-derived class, you would typically enable the events as part of the OnInitialUpdate handler.

But, what if you always want to get these events for a rich edit control, and don't want to be bothered doing this for every rich edit control you add?

In that case, what you do is handle it in the PreSubclassWindow handler of your CRichEditCtrl-derived subclass. PreSubclassWindow is a good place to do lots of useful and interesting "default initializations" that require an actual HWND object exist (as opposed to a constructor, which can be executed far, far before there is an HWND associated with the control). I discuss this in a separate article.

For a CDialog, CPropertyPage, CFormView, etc., your enabling of EN_CHANGE events would be done by the following sequence of operations, assuming that c_MyEdit is a control variable that is bound to the HWND:

c_MyEdit.SetEventMask(ENM_CHANGE | c_MyEdit.GetEventMask());

By the way, has it struck you as a little odd that CRichEditCtrl::GetEventMask returns a long while CRichEditCtrl::SetEventMask requires a DWORD? Do you wonder if anyone ever actually looked at these specifications before publishing them?

Now, we can add a SetWindowTextNoNotify method to our CRichEditCtrl-derived class:

void CMyRichEditCtrl::SetWindowTextNoNotify(LPCTSTR s)
{
    DWORD oldmask = CRichEditCtrl::GetEventMask();
    DWORD newmask = oldmask & ~ENM_CHANGE;
    CRichEditCtrl::SetEventMask(newmask);
    CRichEditCtrl::SetWindowText(s);
    CRichEditCtrl::SetEventMask(oldmask);
}

Because we are able to avoid even generating any EN_CHANGE notification, there is no need to have a reflected handler that will cause them to be ignored. They simply won't happen at all.

In one case, I overrode the SetWindowText method by adding a new method:

void SetWindowText(LPCTSTR s, BOOL notify = TRUE);

and implemented it as:

void CMyRichEditCtrl::SetWindowTAext(LPCTSTR s, BOOL notify /* = TRUE */)
{
    DWORD oldmask;
    DWORD newmask;
    if(!notify)
       {
        oldmask = CRichEditCtrl::GetEventMask();
        newmask = oldmask &~ENM_CHANGE;
        CRichEditCtrl::SetEventMask(newmask);
       }
    CRichEditCtrl::SetWindowText(s);
    if(!notify)
       CRichEditCtrl::SetEventMask(oldmask);
}

I have a slight preference for this latter approach, and it is left as an Exercise For The Reader to implement the corresponding function in the ordinary CEdit control.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here