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:
struct IValidator
{
virtual ~IValidator(void)
{
}
virtual bool Validate(void) = 0;
};
Pretty simple, isn't it? IValidator
exposes the only method - that is pure virtual function Validate()
.
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.
class ValidatorBase
{
protected:
HWND m_hControl;
std::string m_strTitle;
std::string m_strText;
ValidatorBase(HWND hControl, const std::string& strTitle,
const std::string& strText) :
m_hControl(hControl),
m_strTitle(strTitle),
m_strText(strText)
{
}
ValidatorBase(const ValidatorBase& vb) :
m_hControl(vb.m_hControl),
m_strTitle(vb.m_strTitle),
m_strText(vb.m_strText)
{
}
virtual ~ValidatorBase(void)
{
}
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.
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:
bool m_bBase;
public:
GenericTypeValidator(HWND hControl, const std::string& strTitle,
const std::string& strText) :
ValidatorBase(hControl, strTitle, strText),
m_bBase(false)
{
}
GenericTypeValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText)),
m_bBase(false)
{
}
GenericTypeValidator(const GenericTypeValidator& gtv) :
ValidatorBase(gtv),
m_bBase(false)
{
}
virtual ~GenericTypeValidator(void)
{
}
const GenericTypeValidator& operator = (const GenericTypeValidator& gtv)
{
ValidatorBase::operator = (gtv);
m_bBase = gtv.m_bBase;
return *this;
}
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;
}
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
.
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>
{
ValueType m_tLower;
ValueType m_tUpper;
public:
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;
}
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;
}
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 ~GenericRangeValidator(void)
{
}
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;
}
virtual bool Validate(void)
{
if(!GenericTypeValidator<Type, TypeValidator, TypeConverter,
ControlTextProviderPolicy, ErrorReportingPolicy>::Validate())
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
if(Less(m_tValue, m_tLower) || Greater(m_tValue, m_tUpper))
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
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.
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>
{
ValueType m_tBase;
public:
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;
}
GenericComparisonValidator(HWND hControl, UINT nIDTitle,
UINT nIDText, ValueType tBase) :
GenericTypeValidator<Type, TypeValidator,
TypeConverter, ControlTextProviderPolicy,
ErrorReportingPolicy>(hControl, nIDTitle, nIDText),
m_tBase(tBase)
{
m_bBase = true;
}
GenericComparisonValidator(const GenericComparisonValidator& gcv) :
GenericTypeValidator<Type, TypeValidator, TypeConverter,
ControlTextProviderPolicy, ErrorReportingPolicy>(gcv),
m_tBase(gcv.m_tBase)
{
m_bBase = true;
}
virtual ~GenericComparisonValidator(void)
{
}
const GenericComparisonValidator& operator =
(const GenericComparisonValidator& gcv)
{
GenericTypeValidator<Type, TypeValidator, TypeConverter,
ControlTextProviderPolicy,
ErrorReportingPolicy>::operator = (gcv);
m_tBase = true;
return *this;
}
virtual bool Validate(void)
{
if(!GenericTypeValidator<Type, TypeValidator, TypeConverter,
ControlTextProviderPolicy, ErrorReportingPolicy>::Validate())
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
if(!Comparer(m_tBase, m_tValue))
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
ReportSuccess(m_hControl);
return true;
}
};
GenericStringValidator
is a special version (not a specialization) of a validator which handles strings.
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:
GenericStringValidator(HWND hControl,
const std::string& strTitle, const std::string& strText) :
ValidatorBase(hControl, strTitle, strText)
{
}
GenericStringValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText))
{
}
GenericStringValidator(const GenericStringValidator& gsv) :
ValidatorBase(gsv)
{
}
virtual ~GenericStringValidator(void)
{
}
const GenericStringValidator& operator = (const GenericStringValidator& gsv)
{
ValidatorBase::operator = (gsv);
return *this;
}
virtual bool Validate(void)
{
m_tValue = GetControlText(m_hControl);
if(!Validator(m_tValue))
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
ReportSuccess(m_hControl);
return true;
}
};
GenericControlValidator
is supposed to handle various controls by sending them proper messages.
template <bool ControlValidator(HWND),
class ErrorReportingPolicy = MessageBoxErrorReporting,
class ResourceLoaderPolicy = ResourceLoader>
class GenericControlValidator : public ValidatorBase,
public IValidator, public ErrorReportingPolicy,
public ResourceLoaderPolicy
{
public:
GenericControlValidator(HWND hControl,
const std::string& strTitle, const std::string& strText) :
ValidatorBase(hControl, strTitle, strText)
{
}
GenericControlValidator(HWND hControl, UINT nIDTitle, UINT nIDText) :
ValidatorBase(hControl, LoadString(nIDTitle), LoadString(nIDText))
{
}
GenericControlValidator(const GenericControlValidator& gcv) :
ValidatorBase(gcv.m_hControl, gcv.m_strTitle, gcv.m_strText)
{
}
virtual ~GenericControlValidator(void)
{
}
const GenericControlValidator& operator =
(const GenericControlValidator& gcv)
{
ValidatorBase::operator = (gcv);
return *this;
}
virtual bool Validate(void)
{
if(!ControlValidator(m_hControl))
{
ReportError(m_hControl, m_strTitle, m_strText);
return false;
}
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:
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!