Click here to Skip to main content
Click here to Skip to main content

Brand New Validators - Templated Extension Thing

, 25 Nov 2004 CPOL
Rate this:
Please Sign up or sign in to vote.
An extension to standard MFC DDX/DDV mechanism and a new way of data validation in WinAPI programs

Sample image

Introduction

Message boxes are evil. Definitely. They should be the last option considered when it comes to displaying error messages. Boxes are intrusive, they emit pathetic "Ding" sound, and they're somewhat frightening and disturbing. This is the first issue. The second problem was my annoyance with MFC's standard DDX/DDV mechanism. It's very inflexible and allows almost no customization. These were the primary reasons for me to come up with a better method of data validation and, obviously, reporting errors to the user. So here is the full story...

Part One - Base Classes

Well, frankly speaking, that was the first time I did some kind of planning and “drafted” on various pieces of paper my future class hierarchy and stuff. First off, I initially thought of this library as a fully template-based one. At the same time, I needed an interface for all my validator classes. As you will see in the following section, this kind of separation of base classes to templated and non-templated ones is required to implement a container of validators, so here are the classes:

//
// General Validator interface.
//
struct IValidator
{
    //
    // Virtual destructor        
    virtual ~IValidator(void)
    {
    }

    //
    // Validation function. Returns true if validation
    // succeeded
    virtual bool Validate(void) = 0;    
};

Pretty simple, isn't it? IValidator exposes the only method - that is pure virtual function Validate().

//
// Typed validator. Type denotes the actual type of
// the value being validated.
//
template <class Type> struct ITypedValidator : public IValidator
{
    protected:
        typedef typename Type ValueType;
        ValueType m_tValue;
};

ITypedValidator is just an extension to the IValidator interface which introduces a ValueType - essentially the type being validated. It also has m_tValue member variable, which holds the resulting value. Next comes the ValidatorBase class - it holds two message strings and the identifier of the control being validated.

//
// Validator Base. Base class for all validators.
//
class ValidatorBase
{
protected:
    // Control handle
    HWND m_hControl;
    // Error message title
    std::string m_strTitle;
    // Error message text
    std::string m_strText;
    
    //
    // Constructor        
    ValidatorBase(HWND hControl, const std::string& strTitle, 
                                 const std::string& strText) :
        m_hControl(hControl),
        m_strTitle(strTitle),
        m_strText(strText)
    {
    }

    //
    // Copy constructor
    ValidatorBase(const ValidatorBase& vb) :
        m_hControl(vb.m_hControl),
        m_strTitle(vb.m_strTitle),
        m_strText(vb.m_strText)
    {
    }

    //
    // Virtual destructor
    virtual ~ValidatorBase(void)
    {
    }

    //
    // Assignment operator
    const ValidatorBase& operator = (const ValidatorBase& vb)
    {
        m_hControl = vb.m_hControl;
        m_strTitle = vb.m_strTitle;
        m_strText = vb.m_strText;

        return *this;
    }
};

So far, so clear, isn't it?

Part Two - Policies

If you're really interested in advanced C++ techniques, you should definitely read Andrei Alexandrescu's "Modern C++ Design". This is an exceptional book, and it's really worth reading. Along with other topics, it covers Policies - basically, a method of configuring template-based classes. My motivation for using policies was a justified attempt to provide as much flexibility as possible (again, as opposed to MFC).

So currently, there are policies for retrieving text from controls, for loading resources, and for reporting errors to the user. By varying those policies (and writing new ones), you can create a validator, which, for instance, will load string resources from the XML file on a remote server, validate text from the control by exploiting the power of regular expressions, and report errors by writing to the system Event Log (are your errors really that serious?). As you can see, the possibilities are almost endless. By now, there are policies for retrieving text from controls in a usual WinAPI fashion, for loading string resources from executable modules, and for reporting errors to the user via Message Boxes (whoops...) and in a fancy .NET-style way.

Part Three - Validator Classes

So now it's time for a serious business. First, let's see a GenericTypeValidator class.

//
// Generic Type Validator
// Type - Type being validated
// TypeValidator - function, returning true if input
//                 string is a valid representative of the Type
// TypeConverter - conversion function, converts from _TCHAR* to Type
// ControlTextProviderPolicy - Control Text Provider Policy
// ErrorReportingPolicy - Error Reporting Policy
// ResourceLoaderPolicy - Resource Loader Policy
//
template <class Type, bool TypeValidator(const _TCHAR*), 
Type TypeConverter(const _TCHAR*),
class ControlTextProviderPolicy = ControlTextProvider, 
class ErrorReportingPolicy = MessageBoxErrorReporting, 
class ResourceLoaderPolicy = ResourceLoader>
class GenericTypeValidator : public ValidatorBase, 
    public ITypedValidator<Type>, public ControlTextProviderPolicy, 
    public ErrorReportingPolicy, public ResourceLoaderPolicy
{
protected:
    // If this class is a base class
    bool m_bBase;
public:
    //
    // Constructor        
    GenericTypeValidator(HWND hControl, const std::string& strTitle, 
                                       const std::string& strText) :
        ValidatorBase(hControl, strTitle, strText),
        m_bBase(false)
    {
    }

    //
    // Constructor
    GenericTypeValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
        ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText)),
        m_bBase(false)
    {
    }

    //
    // Copy constructor
    GenericTypeValidator(const GenericTypeValidator& gtv) :
        ValidatorBase(gtv),
        m_bBase(false)
    {
    }

    //
    // Virtual destructor
    virtual ~GenericTypeValidator(void)
    {
    }

    //
    // Assignment operator
    const GenericTypeValidator& operator = (const GenericTypeValidator& gtv)
    {
        ValidatorBase::operator = (gtv);

        m_bBase = gtv.m_bBase;
        
        return *this;
    }

    //
    // Validation function        
    virtual bool Validate(void)
    {
        std::string strText = GetControlText(m_hControl);

        if(!TypeValidator(strText.c_str()))
        {
            if(!m_bBase)
                ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        m_tValue = TypeConverter(strText.c_str());

        if(!m_bBase)
            ReportSuccess(m_hControl);

        return true;
    }
};

This validator basically checks whether text in the control is, that's to say, a valid representative of some certain type - be it a floating-point value, an integer value, or anything else.

Now, we have GenericRangeValidator.

//
// Generic Range Validator
// Type - Type being validated
// TypeValidator - function, returning true if input string
//                 is a valid representative of the Type
// TypeConverter - conversion function, converts from _TCHAR* to Type
// Less - returns true if left-hand value is less than right-hand
// Grater - returns true if left-hand value is greater than right-hand
// ControlTextProviderPolicy - Control Text Provider Policy
// ErrorReportingPolicy - Error Reporting Policy
// ResourceLoaderPolicy - Resource Loader Policy
//
template <class Type, bool TypeValidator(const _TCHAR*), 
Type TypeConverter(const _TCHAR*),
bool Less(const Type&, const Type&), bool Greater(const Type&, const Type&),
class ControlTextProviderPolicy = ControlTextProvider, 
class ErrorReportingPolicy = MessageBoxErrorReporting, 
class ResourceLoaderPolicy = ResourceLoader>
class GenericRangeValidator : public GenericTypeValidator<Type, 
    TypeValidator, TypeConverter, ControlTextProviderPolicy, 
    ErrorReportingPolicy, ResourceLoaderPolicy>
{
    // Lower Bound
    ValueType m_tLower;
    // Upper bound
    ValueType m_tUpper;
public:
    //
    // Constructor
    GenericRangeValidator(HWND hControl, const std::string& strTitle, 
             const std::string& strText, ValueType tLower, ValueType tUpper) :
             GenericTypeValidator<Type, TypeValidator, 
             TypeConverter, ControlTextProviderPolicy, 
             ErrorReportingPolicy>(hControl, strTitle, strText),
             m_tLower(tLower), m_tUpper(tUpper)
    {
        m_bBase = true;
    }

    //
    // Constructor
    GenericRangeValidator(HWND hControl, UINT nIDTitle, UINT nIDText, 
        ValueType tLower, ValueType tUpper) :
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
        ControlTextProviderPolicy, 
        ErrorReportingPolicy>(hControl, nIDTitle, nIDText),
        m_tLower(tLower), m_tUpper(tUpper)
    {
        m_bBase = true;
    }

    //
    // Copy constructor
    GenericRangeValidator(const GenericRangeValidator& grv) :
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
        ControlTextProviderPolicy, ErrorReportingPolicy>(grv),
        m_tLower(grv.m_tLower),
        m_tUpper(grv.m_tUpper)
    {
        m_bBase = true;
    }

    //
    // Virtual destructor
    virtual ~GenericRangeValidator(void)
    {
    }

    //
    // Assignment operator
    const GenericRangeValidator& operator = 
          (const GenericRangeValidator& grv)
    {
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
          ControlTextProviderPolicy, ErrorReportingPolicy>::operator = (grv);

        m_tLower = grv.m_tLower;
        m_tUpper = grv.m_tUpper;
        m_bBase = true;

        return *this;
    }

    //
    // Validation function        
    virtual bool Validate(void)
    {
        if(!GenericTypeValidator<Type, TypeValidator, TypeConverter, 
            ControlTextProviderPolicy, ErrorReportingPolicy>::Validate())
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        if(Less(m_tValue, m_tLower) || Greater(m_tValue, m_tUpper))
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        ReportSuccess(m_hControl);

        return true;
    }
};

It is derived from GenericTypeValidator and it checks if m_tValue fits into the specified range. This class can be parameterized, along with other types, with comparison functions. GenericComparisonValidator does almost the same thing as GenericRangeValidator does, but compares m_tValue with one and the only value, thus can be used for validation, for instance, of minimum and maximum values.

//
// Generic Comparison Validator
// Type - Type being validated
// TypeValidator - function, returning true
//                 if input string is a valid representative of the Type
// TypeConverter - conversion function, converts from _TCHAR* to Type
// Comparer - returns true if comparison is correct
// ControlTextProviderPolicy - Control Text Provider Policy
// ErrorReportingPolicy - Error Reporting Policy
// ResourceLoaderPolicy - Resource Loader Policy
//
template <class Type, bool TypeValidator(const _TCHAR*), 
Type TypeConverter(const _TCHAR*),
bool Comparer(const Type&, const Type&), 
class ControlTextProviderPolicy = ControlTextProvider, 
class ErrorReportingPolicy = MessageBoxErrorReporting, 
class ResourceLoaderPolicy = ResourceLoader>
class GenericComparisonValidator : public GenericTypeValidator<Type, 
    TypeValidator, TypeConverter, ControlTextProviderPolicy, 
    ErrorReportingPolicy, ResourceLoaderPolicy>
{
    // Base Value
    ValueType m_tBase;
public:
    //
    // Constructor
    GenericComparisonValidator(HWND hControl, const std::string& strTitle, 
        const std::string& strText, ValueType tBase) :
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
        ControlTextProviderPolicy, 
        ErrorReportingPolicy>(hControl, strTitle, strText),
        m_tBase(tBase)
    {
        m_bBase = true;
    }

    //
    // Constructor
    GenericComparisonValidator(HWND hControl, UINT nIDTitle, 
          UINT nIDText, ValueType tBase) :
          GenericTypeValidator<Type, TypeValidator, 
          TypeConverter, ControlTextProviderPolicy, 
          ErrorReportingPolicy>(hControl, nIDTitle, nIDText),
          m_tBase(tBase)
    {
        m_bBase = true;
    }

    //
    // Copy constructor
    GenericComparisonValidator(const GenericComparisonValidator& gcv) :
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
        ControlTextProviderPolicy, ErrorReportingPolicy>(gcv),
        m_tBase(gcv.m_tBase)
    {
        m_bBase = true;
    }

    //
    // Virtual destructor
    virtual ~GenericComparisonValidator(void)
    {
    }

    //
    // Assignment operator
    const GenericComparisonValidator& operator = 
          (const GenericComparisonValidator& gcv)
    {
        GenericTypeValidator<Type, TypeValidator, TypeConverter, 
           ControlTextProviderPolicy, 
           ErrorReportingPolicy>::operator = (gcv);

        m_tBase = true;

        return *this;
    }

    //
    // Validation function        
    virtual bool Validate(void)
    {
        if(!GenericTypeValidator<Type, TypeValidator, TypeConverter, 
            ControlTextProviderPolicy, ErrorReportingPolicy>::Validate())
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        if(!Comparer(m_tBase, m_tValue))
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        ReportSuccess(m_hControl);

        return true;
    }
};

GenericStringValidator is a special version (not a specialization) of a validator which handles strings.

//
// Generic String Validator
// Validator - returns true if comparison is correct
// ControlTextProviderPolicy - Control Text Provider Policy
// ErrorReportingPolicy - Error Reporting Policy
// ResourceLoaderPolicy - Resource Loader Policy
//
template <bool Validator(const std::string&), 
class ControlTextProviderPolicy = ControlTextProvider, 
class ErrorReportingPolicy = MessageBoxErrorReporting, 
class ResourceLoaderPolicy = ResourceLoader>
class GenericStringValidator : public ValidatorBase, 
    public ITypedValidator<std::string>, 
    public ControlTextProviderPolicy, 
    public ErrorReportingPolicy, 
    public ResourceLoaderPolicy
{
public:
    //
    // Constructor
    GenericStringValidator(HWND hControl, 
        const std::string& strTitle, const std::string& strText) :
        ValidatorBase(hControl, strTitle, strText)
    {
    }

    //
    // Constructor
    GenericStringValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
        ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText))
    {
    }

    //
    // Copy constructor
    GenericStringValidator(const GenericStringValidator& gsv) :
        ValidatorBase(gsv)
    {
    }

    //
    // Virtual destructor
    virtual ~GenericStringValidator(void)
    {
    }

    //
    // Assignment operator
    const GenericStringValidator& operator = (const GenericStringValidator& gsv)
    {
        ValidatorBase::operator = (gsv);

        return *this;
    }

    //
    // Validation function        
    virtual bool Validate(void)
    {
        m_tValue = GetControlText(m_hControl);

        if(!Validator(m_tValue))
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        ReportSuccess(m_hControl);

        return true;
    }
};

GenericControlValidator is supposed to handle various controls by sending them proper messages.

//
// Generic Control Validator
// ControlValidator - function, returning true if the control is in valid state
// ErrorReportingPolicy - Error Reporting Policy
// ResourceLoaderPolicy - Resource Loader Policy
//    
template <bool ControlValidator(HWND), 
class ErrorReportingPolicy = MessageBoxErrorReporting, 
class ResourceLoaderPolicy = ResourceLoader>
class GenericControlValidator : public ValidatorBase, 
    public IValidator, public ErrorReportingPolicy, 
    public ResourceLoaderPolicy
{    
public:
    //
    // Constructor        
    GenericControlValidator(HWND hControl, 
        const std::string& strTitle, const std::string& strText) :
        ValidatorBase(hControl, strTitle, strText)
    {
    }

    //
    // Constructor
    GenericControlValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
        ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText))
    {
    }

    //
    // Copy constructor
    GenericControlValidator(const GenericControlValidator& gcv) :
        ValidatorBase(gcv.m_hControl, gcv.m_strTitle, gcv.m_strText)
    {
    }

    //
    // Virtual destructor
    virtual ~GenericControlValidator(void)
    {
    }

    //
    // Assignment operator
    const GenericControlValidator& operator = 
          (const GenericControlValidator& gcv)
    {
        ValidatorBase::operator = (gcv);

        return *this;
    }

    //
    // Validation function        
    virtual bool Validate(void)
    {
        if(!ControlValidator(m_hControl))
        {
            ReportError(m_hControl, m_strTitle, m_strText);
            return false;
        } // if

        ReportSuccess(m_hControl);

        return true;
    }
};

Part Four - Using 'Em

This particular thing is pretty straightforward. Suppose you have a dialog, the contents of which you're about to validate. Now you have a couple of options. You can either validate everything in a more or less usual way by validating everything in the OnOK handler or perform on-the-fly validation, that is in WM_KICKIDLE message handler. These two choices differ insignificantly, but it's better to use non-intrusive error reporting policy for the latter. All right, let's start coding. First, add validator.h to your project. Now add a Validator::ValidatorPool member variable to your dialog class. You could've added loads of specific validators, but it isn't a huge fun to invoke them by yourself. But anyway - if you want it….. Now we have to add a few validators. That's how it's done:

//
// Initializing validators
m_vpValidators.AddValidator(IDC_EDIT1, 
    IValidatorPtr(new IntegerValidator(*GetDlgItem(IDC_EDIT1), "Integer Value", 
    "Please enter an integer value")), true);
m_vpValidators.AddValidator(IDC_EDIT2, 
    IValidatorPtr(new FloatValidator(*GetDlgItem(IDC_EDIT2), "Floating Point Value", 
    "Please enter a floating point value")), true);
m_vpValidators.AddValidator(IDC_EDIT3, 
    IValidatorPtr(new IntegerInclusiveRangeValidator(*GetDlgItem(IDC_EDIT3), 
    "Ranged Integer Value", 
    "Please enter an integer value ranging from 0 to 542", -1, 543)), true);
m_vpValidators.AddValidator(IDC_EDIT4, 
    IValidatorPtr(new NotEmptyStringValidator(*GetDlgItem(IDC_EDIT4), 
    "Non-Empty String", 
    "Please enter something...")), true);

And now it's time to choose (don't we have to choose all the time?). For the Validate-in-OnOK approach, override OnOK virtual function of your dialog class and add the following line:

// ...
if(!m_vpValidators.Validate())
    return;
// ...

And that's it. Of course, you can toggle some specific validators depending on some conditions (say, a Check Box was checked, or an item in a List Control selected - anything) to disable validation for those specific controls. Here's the second option. Modify your stdafx.h by adding this include statement:

#include <afxpriv.h>

In your dialog header class, add the following prototype:

afx_msg LRESULT OnKickIdle(WPARAM wParam, LPARAM lParam);

Add this entry to the message map:

ON_MESSAGE(WM_KICKIDLE, OnKickIdle)

And implement OnKickIdle this way:

LRESULT CValidatorsDlg::OnKickIdle(WPARAM wParam, LPARAM lParam)
{
    GetDlgItem(IDOK)->EnableWindow(m_vpValidators.Validate());

    return 0;
}

And, again, that's it.

Part Five - Stuff

We're almost finished. First off, I can't promise that this code will compile with all compilers. I wrote it using Visual C++ 7.1 (the one that comes with Microsoft Visual Studio .NET 2003) and there it works just fine. I'd be terribly grateful for any comments concerning portability and compatibility.

And just a few notes about the things I'd like to implement. First of all, regexps - primarily to validate emails, URLs and credit cards. Of course, there is ::PathIsURL API, but why do guys from Redmond consider everything that starts with http:// a valid URL? Dammit, even Pocket Word thinks that this was an URL! Second - a balloon tooltip error reporting policy. And, of course, all your suggestions. Thanks!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

AntonGogolev
Web Developer
Russian Federation Russian Federation
I'll think about it later on...

Comments and Discussions

 
General7-Zip worked Pinmembergoiania4-Jan-05 11:58 
GeneralUnzip the files Pinmembergoiania4-Jan-05 11:17 
GeneralCould not unzip files. PinmemberWREY2-Dec-04 6:56 
GeneralRe: Could not unzip files. PinmemberSl0n2-Dec-04 9:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411023.1 | Last Updated 25 Nov 2004
Article Copyright 2004 by AntonGogolev
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid