// Copyright (C) 2000 Microsoft Corporation // // Dynamic DNS detection/diagnostic page // // 22 Aug 2000 sburns #include "headers.hxx" #include "page.hpp" #include "DynamicDnsPage.hpp" #include "resource.h" #include "state.hpp" HINSTANCE DynamicDnsPage::richEditHInstance = 0; DynamicDnsPage::DynamicDnsPage() : DCPromoWizardPage( IDD_DYNAMIC_DNS, IDS_DYNAMIC_DNS_PAGE_TITLE, IDS_DYNAMIC_DNS_PAGE_SUBTITLE), diagnosticResultCode(UNEXPECTED_FINDING_SERVER), needToKillSelection(false), originalMessageHeight(0), testPassCount(0) { LOG_CTOR(DynamicDnsPage); WSADATA data; DWORD err = ::WSAStartup(MAKEWORD(2,0), &data); // if winsock startup fails, that's a shame. The gethostbyname will // not work, but there's not much we can do about that. ASSERT(!err); if (!richEditHInstance) { // You have to load the rich edit dll to get the window class, etc. // to register. Otherwise the dialog will fail create, and the page // will not appear. HRESULT hr = Win::LoadLibrary(L"riched20.dll", richEditHInstance); ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) { // we've no choice than crash the app. throw Win::Error(hr, IDS_RICHEDIT_LOAD_FAILED); } } } DynamicDnsPage::~DynamicDnsPage() { LOG_DTOR(DynamicDnsPage); ::WSACleanup(); Win::FreeLibrary(richEditHInstance); } void DynamicDnsPage::ShowButtons(bool shown) { LOG_FUNCTION(DynamicDnsPage::ShowButtons); HWND ignoreButton = Win::GetDlgItem(hwnd, IDC_IGNORE); HWND richEdit = Win::GetDlgItem(hwnd, IDC_MESSAGE); int state = shown ? SW_SHOW : SW_HIDE; Win::ShowWindow(Win::GetDlgItem(hwnd, IDC_RETRY), state); Win::ShowWindow(Win::GetDlgItem(hwnd, IDC_INSTALL_DNS), state); Win::ShowWindow(ignoreButton, state); RECT r; Win::GetWindowRect(richEdit, r); // Convert r to coords relative to the parent window. Win::MapWindowPoints(0, hwnd, r); if (shown) { // If we're showing the buttons, collapse the rich edit to it's normal // height. Win::MoveWindow( richEdit, r.left, r.top, r.right - r.left, originalMessageHeight, true); } else { // If we're hiding the buttons, expand the rich edit to include their // real estate. RECT r1; Win::GetWindowRect(ignoreButton, r1); Win::MapWindowPoints(0, hwnd, r1); Win::MoveWindow( richEdit, r.left, r.top, r.right - r.left, r1.bottom - r.top, true); } } void DynamicDnsPage::SelectRadioButton(int buttonResId) { // If the order of the buttons changes, then this must be changed. The // buttons also need to have consecutively numbered res IDs in the tab // order. Win::CheckRadioButton(hwnd, IDC_RETRY, IDC_IGNORE, buttonResId); } void DynamicDnsPage::OnInit() { LOG_FUNCTION(DynamicDnsPage::OnInit); HWND richEdit = Win::GetDlgItem(hwnd, IDC_MESSAGE); // ask for link messages Win::RichEdit_SetEventMask(richEdit, ENM_LINK); // save the normal size of the message box so we can restore it later. RECT r; Win::GetWindowRect(richEdit, r); originalMessageHeight = r.bottom - r.top; Win::SendMessage( richEdit, EM_SETBKGNDCOLOR, 0, Win::GetSysColor(COLOR_BTNFACE)); SelectRadioButton(IDC_IGNORE); // Hide the radio buttons initially ShowButtons(false); multiLineEdit.Init(Win::GetDlgItem(hwnd, IDC_MESSAGE)); // pick the proper radio button label if (State::GetInstance().ShouldConfigDnsClient()) { Win::SetDlgItemText( hwnd, IDC_INSTALL_DNS, IDS_INSTALL_DNS_RADIO_LABEL_WITH_CLIENT); } else { Win::SetDlgItemText( hwnd, IDC_INSTALL_DNS, IDS_INSTALL_DNS_RADIO_LABEL); } } // Adds a trailing '.' to the supplied name if one is not already present. // // name - in, name to add a trailing '.' to, if it doesn't already have one. // If this value is the empty string, then '.' is returned. String FullyQualifyDnsName(const String& name) { LOG_FUNCTION2(FullyQualifyDnsName, name); if (name.empty()) { return L"."; } // needs a trailing dot // REVIEW: name[name.length() - 1] is the same as *(name.rbegin()) // which is cheaper? if (name[name.length() - 1] != L'.') { return name + L"."; } // already has a trailing dot return name; } // Scans a linked list of DNS_RECORDs, returning a pointer to the first record // of type SOA, or 0 if no record of that type is in the list. // // recordList - in, linked list of DNS_RECORDs, as returned from DnsQuery DNS_RECORD* FindSoaRecord(DNS_RECORD* recordList) { LOG_FUNCTION(FindSoaRecord); ASSERT(recordList); DNS_RECORD* result = recordList; while (result) { if (result->wType == DNS_TYPE_SOA) { LOG(L"SOA record found"); break; } result = result->pNext; } return result; } // Returns the textual representation of the IP address for the given server // name, in the form "xxx.xxx.xxx.xxx", or the empty string if not IP address // can be determined. // // serverName - in, the host name of the server for which to find the IP // address. If the value is the empty string, then the empty string is // returned from the function. String GetIpAddress(const String& serverName) { LOG_FUNCTION2(GetIpAddress, serverName); ASSERT(!serverName.empty()); String result; do { if (serverName.empty()) { break; } LOG(L"Calling gethostbyname"); AnsiString ansi; serverName.convert(ansi); HOSTENT* host = gethostbyname(ansi.c_str()); if (host && host->h_addr_list[0]) { struct in_addr a; ::CopyMemory(&a.S_un.S_addr, host->h_addr_list[0], sizeof a.S_un.S_addr); result = inet_ntoa(a); break; } LOG(String::format(L"WSAGetLastError = 0x%1!0X", WSAGetLastError())); } while (0); LOG(result); return result; } // Find the DNS server that is authoritative for registering the given server // name, i.e. what server would register the name. Returns NO_ERROR on // success, or a DNS status code (a win32 error) on failure. On failure, the // out parameters are all empty strings. // // serverName - in, candidate name for registration. This value should not be the // empty string. // // authZone - out, the zone the name would be registered in. // // authServer - out, the name of the DNS server that would have the // registration. // // authServerIpAddress - out, textual representation of the IP address of the // server named by authServer. DNS_STATUS FindAuthoritativeServer( const String& serverName, String& authZone, String& authServer, String& authServerIpAddress) { LOG_FUNCTION2(FindAuthoritativeServer, serverName); ASSERT(!serverName.empty()); authZone.erase(); authServer.erase(); authServerIpAddress.erase(); // ensure that the server name ends with a "." so that we have a stop // point for our loop String currentName = FullyQualifyDnsName(serverName); DNS_STATUS result = NO_ERROR; DNS_RECORD* queryResults = 0; while (!currentName.empty()) { result = MyDnsQuery( currentName, DNS_TYPE_SOA, DNS_QUERY_BYPASS_CACHE, queryResults); if ( result == ERROR_TIMEOUT || result == DNS_ERROR_RCODE_SERVER_FAILURE) { // we bail out entirely LOG(L"failed to find autoritative server."); break; } // search for an SOA RR DNS_RECORD* soaRecord = queryResults ? FindSoaRecord(queryResults) : 0; if (soaRecord) { // collect return values, and we're done. LOG(L"autoritative server found"); authZone = soaRecord->pName; authServer = soaRecord->Data.SOA.pNamePrimaryServer; authServerIpAddress = GetIpAddress(authServer); break; } // no SOA record found. if (currentName == L".") { // We've run out of names to query. This situation is so unlikely // that the DNS server would have to be seriously broken to put // us in this state. So this is almost an assert case. LOG(L"Root zone reached without finding SOA record!"); result = DNS_ERROR_ZONE_HAS_NO_SOA_RECORD; break; } // whack off the leftmost label, and iterate again on the parent // zone. currentName = Dns::GetParentDomainName(currentName); MyDnsRecordListFree(queryResults); queryResults = 0; } MyDnsRecordListFree(queryResults); LOG(String::format(L"result = %1!08X!", result)); LOG(L"authZone = " + authZone); LOG(L"authServer = " + authServer); LOG(L"authServerIpAddress = " + authServerIpAddress); return result; } DNS_STATUS MyDnsUpdateTest(const String& name) { LOG_FUNCTION2(MyDnsUpdateTest, name); ASSERT(!name.empty()); LOG(L"Calling DnsUpdateTest"); LOG( L"hContextHandle : 0"); LOG(String::format(L"pszName : %1", name.c_str())); LOG( L"fOptions : 0"); LOG( L"aipServers : 0"); DNS_STATUS status = ::DnsUpdateTest( 0, const_cast(name.c_str()), 0, 0); LOG(String::format(L"status = %1!08X!", status)); LOG(MyDnsStatusString(status)); return status; } // Returns result code that corresponds to what messages to be displayed and // what radio buttons to make available as a result of the diagnostic. // // Also returns thru out parameters information to be included in the // messages. // // serverName - in, the name of the domain contoller to be registered. // // errorCode - out, the DNS error code (a win32 error) encountered when // running the diagnostic. // // authZone - out, the zone the name would be registered in. // // authServer - out, the name of the DNS server that would have the // registration. // // authServerIpAddress - out, textual representation of the IP address of the // server named by authServer. DynamicDnsPage::DiagnosticCode DynamicDnsPage::DiagnoseDnsRegistration( const String& serverName, DNS_STATUS& errorCode, String& authZone, String& authServer, String& authServerIpAddress) { LOG_FUNCTION(DynamicDnsPage::DiagnoseDnsRegistration); ASSERT(!serverName.empty()); DiagnosticCode result = UNEXPECTED_FINDING_SERVER; errorCode = FindAuthoritativeServer( serverName, authZone, authServer, authServerIpAddress); switch (errorCode) { case NO_ERROR: { if (authZone == L".") { // Message 8 LOG(L"authZone is root"); result = ZONE_IS_ROOT; } else { errorCode = MyDnsUpdateTest(serverName); switch (errorCode) { case DNS_ERROR_RCODE_NO_ERROR: case DNS_ERROR_RCODE_YXDOMAIN: { // Message 1 LOG(L"DNS registration support verified."); result = SUCCESS; break; } case DNS_ERROR_RCODE_NOT_IMPLEMENTED: case DNS_ERROR_RCODE_REFUSED: { // Message 2 LOG(L"Server does not support update"); result = SERVER_CANT_UPDATE; break; } default: { // Message 3 result = ERROR_TESTING_SERVER; break; } } } break; } case DNS_ERROR_RCODE_SERVER_FAILURE: { // Message 6 result = ERROR_FINDING_SERVER; break; } case ERROR_TIMEOUT: { // Message 11 result = TIMEOUT; break; } default: { // Message 4 LOG(L"Unexpected error"); result = UNEXPECTED_FINDING_SERVER; break; } } LOG(String::format(L"DiagnosticCode = %1!x!", result)); return result; } // void // DumpSel(HWND richEdit) // { // CHARRANGE range; // RichEdit_GetSel(richEdit, range); // // LOG(String::format("cpMin = %1!d! cpMax = %1!d!", range.cpMin, range.cpMax)); // } void DynamicDnsPage::UpdateMessageWindow(const String& message) { LOG_FUNCTION(UpdateMessageWindow); ASSERT(!message.empty()); // this should have been set before we get here ASSERT(!details.empty()); HWND richEdit = Win::GetDlgItem(hwnd, IDC_MESSAGE); // Clear out the window of any prior contents. This is needed because in // the code that follows, we take advantage of the fact that the set text // functions create an empty selection to the end of the text, and // subsequent ST_SELECTION type calls to set text will append at that // point. Win::RichEdit_SetText(richEdit, ST_DEFAULT, L""); static const String RTF_HEADER_ON( L"{\\rtf" // RTF header L"\\pard" // start default paragraph style L"\\sa100" // whitespace after paragraph = 100 twips L"\\b "); // bold on static const String RTF_HEADER_OFF( L"\\b0" // bold off L"\\par " // end paragraph L"}"); // end RTF Win::RichEdit_SetRtfText( richEdit, ST_SELECTION, RTF_HEADER_ON + String::load(IDS_DIAGNOSTIC_RESULTS) + RTF_HEADER_OFF); Win::RichEdit_SetText( richEdit, ST_SELECTION, String::format( (testPassCount == 1) ? IDS_DIAGNOSTIC_COUNTER_1 : IDS_DIAGNOSTIC_COUNTER_N, testPassCount) + L"\r\n\r\n" + message + L"\r\n\r\n"); if (!helpTopicLink.empty()) { // We have help to show, so insert a line with a link to click for it. Win::RichEdit_SetText( richEdit, ST_SELECTION, String::load(IDS_DIAGNOSTIC_HELP_LINK_PRE)); // At this point, we want to insert the help link and set it to the link // style. We do this by tracking the position of the link text, and // then selecting that text, and then setting the selection to the link // style. CHARRANGE beginRange; Win::RichEdit_GetSel(richEdit, beginRange); Win::RichEdit_SetText( richEdit, ST_SELECTION, String::load(IDS_DIAGNOSTIC_HELP_LINK)); CHARRANGE endRange; Win::RichEdit_GetSel(richEdit, endRange); ASSERT(endRange.cpMin > beginRange.cpMax); Win::Edit_SetSel(richEdit, beginRange.cpMax, endRange.cpMin); CHARFORMAT2 format; // REVIEWED-2002/02/26-sburns correct byte count passed. ::ZeroMemory(&format, sizeof format); format.dwMask = CFM_LINK; format.dwEffects = CFE_LINK; Win::RichEdit_SetCharacterFormat(richEdit, SCF_SELECTION, format); // set the selection back to the end of where the link was inserted. Win::RichEdit_SetSel(richEdit, endRange); // now continue to the end Win::RichEdit_SetText( richEdit, ST_SELECTION, String::load(IDS_DIAGNOSTIC_HELP_LINK_POST) + L"\r\n\r\n"); } Win::RichEdit_SetRtfText( richEdit, ST_SELECTION, RTF_HEADER_ON + String::load(IDS_DETAILS) + RTF_HEADER_OFF); Win::RichEdit_SetText(richEdit, ST_SELECTION, details + L"\r\n\r\n"); } // do the test, update the text on the page, update the radio buttons // enabled state, choose a radio button default if neccessary void DynamicDnsPage::DoDnsTestAndUpdatePage() { LOG_FUNCTION(DynamicDnsPage::DoDnsTestAndUpdatePage); // this might take a while. Win::WaitCursor cursor; State& state = State::GetInstance(); String domain = state.GetNewDomainDNSName(); DNS_STATUS errorCode = 0; String authZone; String authServer; String authServerIpAddress; String serverName = L"_ldap._tcp.dc._msdcs." + domain; diagnosticResultCode = DiagnoseDnsRegistration( serverName, errorCode, authZone, authServer, authServerIpAddress); ++testPassCount; String message; int defaultButton = IDC_IGNORE; switch (diagnosticResultCode) { // Message 1 case SUCCESS: { message = String::load(IDS_DYN_DNS_MESSAGE_SUCCESS); String errorMessage; if (errorCode == DNS_ERROR_RCODE_YXDOMAIN) { // NTRAID#NTBUG9-586579-2002/04/15-sburns errorMessage = String::format( IDS_DNS_ERROR_RCODE_YXDOMAIN_ADDENDA, serverName.c_str(), authZone.c_str(), serverName.c_str()); } else { errorMessage = GetErrorMessage(Win32ToHresult(errorCode)); } details = String::format( // NTRAID#NTBUG9-485456-2001/10/24-sburns IDS_DYN_DNS_DETAIL_FULL_SANS_CODE, authServer.c_str(), authServerIpAddress.c_str(), authZone.c_str(), errorMessage.c_str()); helpTopicLink = L""; defaultButton = IDC_IGNORE; ShowButtons(false); break; } // Message 2 case SERVER_CANT_UPDATE: { message = String::load(IDS_DYN_DNS_MESSAGE_SERVER_CANT_UPDATE); details = String::format( IDS_DYN_DNS_DETAIL_FULL, authServer.c_str(), authServerIpAddress.c_str(), authZone.c_str(), GetErrorMessage(Win32ToHresult(errorCode)).c_str(), errorCode, MyDnsStatusString(errorCode).c_str()); if (Dns::CompareNames(authZone, domain) == DnsNameCompareEqual) { helpTopicLink = L"DNSConcepts.chm::/sag_DNS_tro_dynamic_message2a.htm"; } else { helpTopicLink = L"DNSConcepts.chm::/sag_DNS_tro_dynamic_message2b.htm"; } defaultButton = IDC_RETRY; ShowButtons(true); break; } // Message 3 case ERROR_TESTING_SERVER: { message = String::load(IDS_DYN_DNS_MESSAGE_ERROR_TESTING_SERVER); details = String::format( IDS_DYN_DNS_DETAIL_FULL, authServer.c_str(), authServerIpAddress.c_str(), authZone.c_str(), GetErrorMessage(Win32ToHresult(errorCode)).c_str(), errorCode, MyDnsStatusString(errorCode).c_str()); helpTopicLink = "DNSConcepts.chm::/sag_DNS_tro_dynamic_message3.htm"; defaultButton = IDC_RETRY; ShowButtons(true); break; } // Message 6 case ERROR_FINDING_SERVER: { ASSERT(authServer.empty()); ASSERT(authZone.empty()); ASSERT(authServerIpAddress.empty()); message = String::load(IDS_DYN_DNS_MESSAGE_ERROR_FINDING_SERVER); details = String::format( IDS_DYN_DNS_DETAIL_SCANT, serverName.c_str(), GetErrorMessage(Win32ToHresult(errorCode)).c_str(), errorCode, MyDnsStatusString(errorCode).c_str()); helpTopicLink = "DNSConcepts.chm::/sag_DNS_tro_dynamic_message6.htm"; defaultButton = IDC_INSTALL_DNS; ShowButtons(true); break; } // Message 8 case ZONE_IS_ROOT: { message = String::format( IDS_DYN_DNS_MESSAGE_ZONE_IS_ROOT, domain.c_str(), domain.c_str()); details = String::format( IDS_DYN_DNS_DETAIL_ROOT_ZONE, authServer.c_str(), authServerIpAddress.c_str()); helpTopicLink = L"DNSConcepts.chm::/sag_DNS_tro_dynamic_message8.htm"; defaultButton = IDC_INSTALL_DNS; ShowButtons(true); break; } // Message 11 case TIMEOUT: { message = String::load(IDS_DYN_DNS_MESSAGE_TIMEOUT); details = String::format( IDS_DYN_DNS_DETAIL_SCANT, serverName.c_str(), GetErrorMessage(Win32ToHresult(errorCode)).c_str(), errorCode, MyDnsStatusString(errorCode).c_str()); helpTopicLink = L"DNSConcepts.chm::/sag_DNS_tro_dynamic_message11.htm"; defaultButton = IDC_INSTALL_DNS; ShowButtons(true); break; } // Message 4 case UNEXPECTED_FINDING_SERVER: // Anything else default: { #ifdef DBG ASSERT(authServer.empty()); ASSERT(authZone.empty()); ASSERT(authServerIpAddress.empty()); if (diagnosticResultCode != UNEXPECTED_FINDING_SERVER) { ASSERT(false); } #endif message = String::load(IDS_DYN_DNS_MESSAGE_UNEXPECTED); details = String::format( IDS_DYN_DNS_DETAIL_SCANT, serverName.c_str(), GetErrorMessage(Win32ToHresult(errorCode)).c_str(), errorCode, MyDnsStatusString(errorCode).c_str()); helpTopicLink = L"DNSConcepts.chm::/sag_DNS_tro_dynamic_message4.htm"; defaultButton = IDC_RETRY; ShowButtons(true); break; } } UpdateMessageWindow(message); // success always forces the ignore option if (diagnosticResultCode == SUCCESS) { SelectRadioButton(IDC_IGNORE); } else { // On the first pass only, decide what radio button to set. On // subsequent passes, the user will have had the chance to change the // button selection, so we don't change his selections. if (testPassCount == 1) { int button = defaultButton; ASSERT(diagnosticResultCode != SUCCESS); // if the test failed, and the wizard is running unattended, then // consult the answer file for the user's preference in dealing // with the failure. if (state.UsingAnswerFile()) { String option = state.GetAnswerFileOption(AnswerFile::OPTION_AUTO_CONFIG_DNS); if ( option.empty() || (option.icompare(AnswerFile::VALUE_YES) == 0) ) { button = IDC_INSTALL_DNS; } else { button = IDC_IGNORE; } } SelectRadioButton(button); } } } bool DynamicDnsPage::OnSetActive() { LOG_FUNCTION(DynamicDnsPage::OnSetActive); State& state = State::GetInstance(); State::Operation oper = state.GetOperation(); // these are the only operations for which this page is valid; i.e. // new domain scenarios if ( oper == State::FOREST || oper == State::CHILD || oper == State::TREE) { DoDnsTestAndUpdatePage(); needToKillSelection = true; } if ( ( oper != State::FOREST && oper != State::CHILD && oper != State::TREE) || state.RunHiddenUnattended() ) { LOG(L"Planning to Skip DynamicDnsPage"); Wizard& wizard = GetWizard(); if (wizard.IsBacktracking()) { // backup once again wizard.Backtrack(hwnd); return true; } int nextPage = Validate(); if (nextPage != -1) { LOG(L"skipping DynamicDnsPage"); wizard.SetNextPageID(hwnd, nextPage); return true; } state.ClearHiddenWhileUnattended(); } Win::PropSheet_SetWizButtons( Win::GetParent(hwnd), PSWIZB_BACK | PSWIZB_NEXT); return true; } void DumpButtons(HWND dialog) { LOG(String::format(L"retry : (%1)", Win::IsDlgButtonChecked(dialog, IDC_RETRY) ? L"*" : L" ")); LOG(String::format(L"ignore : (%1)", Win::IsDlgButtonChecked(dialog, IDC_IGNORE) ? L"*" : L" ")); LOG(String::format(L"install: (%1)", Win::IsDlgButtonChecked(dialog, IDC_INSTALL_DNS) ? L"*" : L" ")); } int DynamicDnsPage::Validate() { LOG_FUNCTION(DynamicDnsPage::Validate); int nextPage = -1; do { State& state = State::GetInstance(); State::Operation oper = state.GetOperation(); DumpButtons(hwnd); if ( oper != State::FOREST && oper != State::CHILD && oper != State::TREE) { // by definition valid, as the page does not apply State::GetInstance().SetAutoConfigureDNS(false); nextPage = IDD_RAS_FIXUP; break; } if ( diagnosticResultCode == SUCCESS || Win::IsDlgButtonChecked(hwnd, IDC_IGNORE)) { // You can go about your business. Move along, move long. // Force ignore, even if the user previously had encountered a // failure and chose retry or install DNS. We do this in case the // user backed up in the wizard and corrected the domain name. State::GetInstance().SetAutoConfigureDNS(false); nextPage = IDD_RAS_FIXUP; break; } // if the radio button selection = retry, then do the test over again, // and stick to this page. if (Win::IsDlgButtonChecked(hwnd, IDC_RETRY)) { DoDnsTestAndUpdatePage(); break; } ASSERT(Win::IsDlgButtonChecked(hwnd, IDC_INSTALL_DNS)); State::GetInstance().SetAutoConfigureDNS(true); nextPage = IDD_RAS_FIXUP; break; } while (0); LOG(String::format(L"nextPage = %1!d!", nextPage)); return nextPage; } bool DynamicDnsPage::OnWizBack() { LOG_FUNCTION(DynamicDnsPage::OnWizBack); // make sure we reset the auto-config flag => the only way it gets set // it on the 'next' button. State::GetInstance().SetAutoConfigureDNS(false); return DCPromoWizardPage::OnWizBack(); } bool DynamicDnsPage::OnCommand( HWND windowFrom, unsigned controlIdFrom, unsigned code) { bool result = false; switch (controlIdFrom) { case IDC_MESSAGE: { switch (code) { case EN_SETFOCUS: { if (needToKillSelection) { // kill the text selection Win::Edit_SetSel(windowFrom, 0, 0); needToKillSelection = false; result = true; } break; } case MultiLineEditBoxThatForwardsEnterKey::FORWARDED_ENTER: { // our subclasses mutli-line edit control will send us // WM_COMMAND messages when the enter key is pressed. We // reinterpret this message as a press on the default button of // the prop sheet. // This workaround from phellyar. // NTRAID#NTBUG9-232092-2000/11/22-sburns HWND propSheet = Win::GetParent(hwnd); int defaultButtonId = Win::Dialog_GetDefaultButtonId(propSheet); // we expect that there is always a default button on the prop sheet ASSERT(defaultButtonId); Win::SendMessage( propSheet, WM_COMMAND, MAKELONG(defaultButtonId, BN_CLICKED), 0); result = true; break; } default: { // do nothing break; } } break; } default: { // do nothing break; } } return result; } bool DynamicDnsPage::OnNotify( HWND /* windowFrom */ , UINT_PTR controlIDFrom, UINT code, LPARAM lParam) { bool result = false; if (controlIDFrom == IDC_MESSAGE) { switch (code) { case EN_LINK: { ENLINK *enlink = reinterpret_cast(lParam); if (enlink && enlink->msg == WM_LBUTTONUP) { ASSERT(!helpTopicLink.empty()); if (!helpTopicLink.empty()) { Win::HtmlHelp(hwnd, helpTopicLink, HH_DISPLAY_TOPIC, 0); } } break; } default: { // do nothing break; } } } return result; }