Let's now look at another example, one where the Windows Forms user control is used as a dialog, not just part of one.

For that purpose, I have replaced the “Pen Widths” dialog in the Scribble application with a user control based one.

In Scribble.rc, we’ll find the dialog we’ve created that will be the container for the user control:

IDD_DIALOG1 DIALOGEX 0, 0, 186, 90
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
END

Before showing you the user code in ManagedPenWidths.h, there are a couple of points I want to comment about:

  1. Obviously, a user control can be written in any .NET language. I already showed C# in my previous blog entry. This one is in C++ which has the same IDE Visual Designer as its 2 friends.
    As the PM for this feature told me: "One of the main advantages of using WinForms in C++ is the ability to pass C++ types directly into the WinForms control".
    I’m not showing this here.
  2. Unlike a Win32 dialog or a Windows Forms form, a user control does not have the concept of a title. The trick is to set the UserControl.Text.
  3. A user control does not have the concepts of an OK nor a Cancel button. So we have to explicitly use delegates for those so that your code on the native type side (MFC application) can react to the user clicking on them.
  4. Note that I don’t need to test if the events are null before invoking them. The C++ compiler implements the raise method under the cover and test if the backing store for the event is null before calling it. Good opportunity to use ILDasm or .NET Reflector and look at the IL generated!

   public ref class DialogAsUserControl : public System::Windows::Forms::UserControl
   {
      public:
         DialogAsUserControl()
         {
            InitializeComponent();
            Text = "Pen Widths (Managed)" ;
            okButton->Click += gcnew EventHandler( this, & DialogAsUserControl::ButtonPressed ) ;
            cancelButton->Click += gcnew EventHandler( this, & DialogAsUserControl::ButtonPressed ) ;
            defaultButton->Click += gcnew EventHandler( this, & DialogAsUserControl::ButtonPressed ) ;
            thinPenWidthTextBox->Validating += gcnew CancelEventHandler( this, & DialogAsUserControl::OnThinWidthValidating ) ;
            thickPenWidthTextBox->Validating += gcnew CancelEventHandler( this, & DialogAsUserControl::OnThickWidthValidating ) ;
         }

         event EventHandler^ OnOK ;
         event EventHandler^ OnCancel ;
         event EventHandler^ OnDefault ;

         property int ThinWidth
         {
            void set ( int thinWidth ) { thinPenWidthTextBox->Text = thinWidth.ToString() ; }
            int get  () { return Convert::ToInt32( thinPenWidthTextBox->Text ) ; }
         }

         property int ThickWidth
         {
            void set ( int thickWidth ) { thickPenWidthTextBox->Text = thickWidth.ToString() ; }
            int get  () { return Convert::ToInt32( thickPenWidthTextBox->Text ) ; }
         }

      protected :
         void ButtonPressed( Object ^ sender, EventArgs ^ e ) // Let's not directly expose the Windows Forms controls outside the UserControl
         {
            if ( sender == okButton )
               OnOK( this, e ) ;
            if ( sender == cancelButton )
               OnCancel( this, e ) ;
            if ( sender == defaultButton )
               OnDefault( this, e ) ;
         }
        
         // ===========================================================================================

         void OnThinWidthValidating( Object ^ /*sender*/, CancelEventArgs ^ e )
         {
            try
            {
               if ( ThinWidth < 1 || ThinWidth > 512 )
               {
                  errorProvider->SetError( thinPenWidthTextBox, "Width must be between 1 and 512" ) ;
                  e->Cancel = true ;
               }
            }
            catch ( Exception ^ exception )
            {
               errorProvider->SetError( thinPenWidthTextBox, exception->Message ) ;
               e->Cancel = true ;
            }
            finally
            {
               okButton->Enabled = ! e->Cancel ;
               if ( e->Cancel )
                  thinPenWidthTextBox->Select( 0, thinPenWidthTextBox->Text->Length );
               else
                  errorProvider->Clear() ;
            }
         }

         // ===========================================================================================

         void OnThickWidthValidating( Object ^ /*sender*/, CancelEventArgs ^ e )
         {
            try
            {
               if ( ThickWidth < 1 || ThickWidth > 512 )
               {
                  errorProvider->SetError( thickPenWidthTextBox, "Width must be between 1 and 512" ) ;
                  e->Cancel = true ;
               }
               else if ( ThickWidth <= ThinWidth )
               {
                  errorProvider->SetError( thickPenWidthTextBox, "Thick width must be bigger than the thin one!" ) ;
                  e->Cancel = true ;
               }
            }
            catch ( Exception ^ exception )
            {
               errorProvider->SetError( thickPenWidthTextBox, exception->Message ) ;
               e->Cancel = true ;
            }
            finally
            {
               okButton->Enabled = ! e->Cancel ;
               if ( e->Cancel )
                  thickPenWidthTextBox->Select( 0, thickPenWidthTextBox->Text->Length );
               else
                  errorProvider->Clear() ;
            }
         }

We are going to look in ScribDoc.cpp now. Note that I created a native type to work around an issue with CWinFormsDialog<>: it instantiates the user control class (ManagedPenWidths::DialogAsUserControl) when DoModal() is called.

This means we cannot access user control fields/properties/events, even if those are not related to the visual aspect of the control. I don’t know if this behaviour will change before we release Visual C++ 2005 but I sure hope it will.

#include <afxwinforms.h>
using namespace Microsoft::VisualC::MFC ;
using namespace System ;

// =================================================================================================

// We want to add a layer between CWinFormsDialog<> and the developer so that it can be easily used like an MFC dialog...

class ManagedPenWidthsDlg : public CWinFormsDialog<ManagedPenWidths::DialogAsUserControl>
{
   public:
      ManagedPenWidthsDlg() : CWinFormsDialog<ManagedPenWidths::DialogAsUserControl>(IDD_DIALOG1)
      {
      }

      int m_nThinWidth ;
      int m_nThickWidth ;

      BEGIN_DELEGATE_MAP( ManagedPenWidthsDlg )
         EVENT_DELEGATE_ENTRY( OnOK, System::Object^, System::EventArgs^ )
         EVENT_DELEGATE_ENTRY( OnDefault, System::Object^, System::EventArgs^ )
         EVENT_DELEGATE_ENTRY( OnCancel, System::Object^, System::EventArgs^ )
      END_DELEGATE_MAP()

   private:
      void OnOK( Object^ sender, EventArgs^ e )
      {
            m_nThinWidth = GetControl()->ThinWidth ;
            m_nThickWidth = GetControl()->ThickWidth ;
            EndDialog( IDOK ) ;
      }

      void OnCancel( Object^ sender, EventArgs^ e )
      {
         EndDialog( IDCANCEL ) ;
      }

      void OnDefault( Object^ sender, EventArgs^ e )
      {
         GetControl()->ThinWidth = 2 ;
         GetControl()->ThickWidth = 5 ;
      }

      virtual BOOL OnInitDialog()
      {
         if ( CWinFormsDialog<ManagedPenWidths::DialogAsUserControl>::OnInitDialog() )
         {
            GetControl()->OnOK += MAKE_DELEGATE( System::EventHandler, OnOK );
            GetControl()->OnCancel += MAKE_DELEGATE( System::EventHandler, OnCancel );
            GetControl()->OnDefault += MAKE_DELEGATE( System::EventHandler, OnDefault );
            GetControl()->ThinWidth = m_nThinWidth ;
            GetControl()->ThickWidth = m_nThickWidth ;
            return TRUE ;
         }
         return FALSE;
      }
} ;

// =================================================================================================

void CScribbleDoc::OnPenWidths()
{
#ifndef ORIGINAL_MFC_CODE
   AfxEnableControlContainer() ; // Should be in CScribbleApp::InitInstance() !
   ManagedPenWidthsDlg dlg ;
#else
   CPenWidthsDlg dlg;
#endif
   dlg.m_nThinWidth = m_nThinWidth;
   dlg.m_nThickWidth = m_nThickWidth;
   if ( dlg.DoModal() == IDOK )
   {
      m_nThinWidth = dlg.m_nThinWidth;
      m_nThickWidth = dlg.m_nThickWidth;
      ReplacePen();
   }
}

As you can see, you could argue that even here, the user control is still part of a MFC/Win32 dialog. However, you can not only set the title of the dialog from inside the user control but when you resize the dialog, the user control is also resized to fit the whole dialog client area.

For your information, this resizing feature has a bug in Beta 2 and does not work as it should.

Still interested?