Brand New Validators - Templated Extension Thing






4.26/5 (7 votes)
An extension to standard MFC DDX/DDV mechanism and a new way of data validation in WinAPI programs
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!