WinForm과 WPF MessageBox.Show 메서드의 동작 차이

사소하지만, 큰 차이를 우연히 하나 발견하게 되었습니다.

최근에 릴리즈한 식탁보 0.5.7 버전에서 이런 버그 제보가 이슈로 등록되었었는데요,

자세히 살펴보니 MessageBox.Show 메서드의 동작이 WinForm과 WPF가 서로 달랐습니다. 구체적으로, 양쪽 모두 부모 윈도우 핸들을 지정하는 파라미터가 있는데, WinForm의 경우 여기에 null 레퍼런스를 지정하더라도 연결된 부모 윈도우 핸들이 없는 메시지 박스를 띄우도록 동작하지만, WPF의 경우는 그렇지 않았습니다.

내부 코드 구현을 살펴보니 그 이유가 잘 보였습니다.

https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/MessageBox.cs,b0b05e2016dddf55

부모 윈도우를 지정하는 부분에서 null 참조를 대입하면,

https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Interop/WindowInteropHelper.cs,fdbf322c4b24b279

위의 코드와 같이 예외를 발생시킵니다.

https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/MessageBox.cs,61ad43de2de82769,references

반면에 부모 윈도우 핸들 파라미터를 지정하지 않는 부분에서는 처음부터 IntPtr.Zero 핸들을 지정하도록 되어있어서 NPE를 만나지 않습니다.

https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/MessageBox.cs,456

Windows Forms의 경우 부모 윈도우가 null 참조나 IntPtr.Zero로 지정되더라도, 위의 코드와 같이 GetActiveWindow API를 이용하여 대체 핸들을 찾는 동작이 있었습니다.

혹시나 싶어서 찾아봤는데, 이 부분에 대한 Exception 발생이 될 수 있다는 내용은 찾지 못했습니다.

이와 같이 잘 동작할 것이라고 믿었던 코드가 안될 때 당황할 수 있고, 그 떄문에 불필요하게 방어적인 코드를 넣게 될수도 있습니다. 이런 경우 레퍼런스 소스 코드를 둘러보면서 정확한 차이점을 파악하여 문제를 진단하고 해결할 수 있습니다.

평소에 동작하지 않는 코드 때문에 씨름하고 계시다면, 이와 같이 분석을 시도해보면서 방법을 찾아보시면 어떨까 싶습니다!

11개의 좋아요

예전에 oldnewthing의 대화창 관련한 글이 생각나는데요, ^^

Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유 (sysnet.pe.kr)

그 글의 원문 2개(Modality, part 4: The importance of setting the correct owner for modal UI - The Old New Thing, Modality, part 5: Setting the correct owner for modal UI - The Old New Thing)을 보면 Modal 대화창의 owner를 null로 설정하면 의도치 않은 버그가 발생할 수 있다는 글이 나옵니다.

아마도 (순전히 가정입니다.) Windows Forms의 경우에는 fallback 차원에서 그런 코드를 작성한 듯하고, WPF의 경우에는 (당시) 새로 나온 프레임워크이니 아예 그것을 막은 것이 아닌가 추측만 해봅니다.

어쨌든, (MessageBox와 같은) modal 대화창에 null 부모를 지정하는 것은 권장하지 않는다고 합니다.

5개의 좋아요

그렇군요. WinForm의 경우 부모 지정을 안하고 불렀거나, 부모 지정을 null로 불러도 결국은 GetActiveWindow() API를 불러서 결손된 부분을 메웠더군요.

WPF는 깊이 들어가보진 않았지만, 부모 지정을 안하고 부르는 API는 정작 IntPtr.Zero 포인터를 던졌고, 부모 지정을 하는 파라미터에서는 null check를 직접 하지 않고 뒤늦게 내부 코드에서 NPE를 던지는 식이었는데, 심지어 이 마저도 docs에는 그런 예외가 생길 수 있다는 내용이 보이지 않아서 관성대로 썼다가 예상하지 못한 곳에서 만난 오류가 된 것 같습니다. ㅎㅎ

말씀해주신대로 이제는 그렇게 코드 동작이 굳어버리기도 했고, 또 부모를 null 참조로 지정하지 않도록 맞추는게 best practice긴 하지만, 문서화 부재가 조금 아쉽다는 생각이 들었습니다.

3개의 좋아요