Programming/Dot.NET

IE에서 닷넷 스마트 클라이언트 개발2-인터넷 익스플로러와 연동하기2

bcheul 2008. 2. 25. 13:53

출처 블로그 .. 맨 땅에 헤딩~

IE에서 닷넷 스마트 클라이언트 개발2-인터넷 익스플로러와 연동하기2


인터넷 익스플로러와 연동하기 - 2
  저 자 : 정성태
  출판일 : 2004년 3월호

 
IWebBrowser2 인터페이스 얻기
이번 연재의 하이라이트가 아닐까 생각된다. 필자가 처음 스마트 클라이언트를 제작하고서 가장 먼저 해결하고자 했던 것이 바로 자신을 호스팅하고 있는 IWebBrowser2 인터페이스를 얻어내는 것이다. 아마도 이미 개발된 기존 액티브X 중에서도 많이 활용하고 있는 부분이 아닐까 싶다. 액티브X 컨트롤로 구현할 때의 얘기를 해보면, WebBrowser 개체는 자신이 호스팅하고 있는 모든 컨트롤에 대해서 CreateInstance로 개체를 생성한 후 IUnknown::QueryInterface를 통해 IObjectWithSite 인터페이스 포인터를 얻어낸다. 만약 그 인터페이스가 구현되어 있다면 개체의 Site IUnknown 인터페이스 포인터를 인자로 해서 IObjectWithSite::SetSite 메쏘드를 호출해 주고, 결과적으로 액티브X 컨트롤은 넘겨받은 Site 인터페이스 포인터를 통해 자신을 호스팅하고 있는 IWebBrowser2 인터페이스 포인터를 얻어낼 수 있게 되는 것이다.
액티브X에서 구현처럼 처음에 스마트 클라이언트 역시 IObjectWithSite 인터페이스를 구현해 단순히 클래스에서 상속을 받아주면 되지 않을까 싶었는데 성공하지 못했다. 아직 필자가 RCW/CCW의 운영에 대한 지식이 미흡해서 밝히지 못했음을 미리 말해둔다. ‘이달의 디스켓’에 실린 소스에서는 IObjectWithSite 인터페이스 코드를 실어 뒀으니 독자들도 도전해 보기 바란다.
가장 표준적일 수 있는 IObjectWitheSite::SetSite 인터페이스를 통한 방법이 미흡한 실력으로 인해 구현할 수 없지만, 프로그래밍 세계에서는 어떤 문제 해결을 위한 방법이 오직 하나만 있는 것이 아님을 독자들도 잘 알고 있을 것이다. 그렇다면 과연 뭐가 남아 있는가? 이렇게 되면 역시 예전의 unmanaged 환경에서 제공되던 길을 찾아야 한다. 나름대로 여러 가지 방법이 있을 수 있겠지만, 필자가 발견한 방법은 Microsoft Active Accessibility SDK(이하, MSAA)에서 제공해 주는 ObjectFromLresult 함수를 이용한 방법이다.
이것은 해당 응용 프로그램이 고유하게 정의한 윈도우 메시지를 통해 프로세스 간에도 사용할 수 있도록 완전히 마샬링된 인터페이스 포인터를 얻을 수 있는 방법을 제공해 준다. 그렇다고 해서 모든 액티브X 컨트롤이 기본으로 제공하는 것은 아니고, 응용 프로그램 개발자가 알아서 내부에 구현해 줘야 한다. 다행히도 웹 브라우저 컨트롤의 경우 이 방법을 제공하고 있는데, 아마도 이 기능을 넣어둔 MS 개발자조차도 닷넷 환경의 스마트 클라이언트에서 유용하게 쓰일 것이라고는 예측하지 못했을 것 같다.
접근 방법은 정해졌으니 하나씩 구현을 해보자. 우선 해야 할 것은 IWebBrowser2 개체의 윈도우 핸들을 알아내는 것이다. 이는 스마트 클라이언트가 윈도우이고 그의 부모 윈도우가 WebBrowser라는 것으로 쉽게 구할 수 있다. <화면 1>은 Spy ++ 유틸리티를 이용해 웹 브라우저 안에 활성화된 스마트 클라이언트의 위치를 확인해 본 것이다.
지난 호에 만들어 두었던 스마트 클라이언트인 TreeControl의 경우 윈도우 폼 위에 TreeView 컨트롤을 얹어 놓은 형태이다. <화면 1>을 보면 강조된 ‘WindowsForms10.Window8.app9’가 바로 윈도우 폼이고, 하위의 ‘WindowsForms10.SysTreeView32.app9’가 TreeView 컨트롤이다. 우리가 구해야 할 IWebBrowser2 인터페이스 포인터를 담고 있는 윈도우는 스마트 클라이언트의 부모 윈도우인 ‘Internet Explorer_Server’이기 때문에 Win32 API에서 제공하고 있는 GetParent API로 구할 수 있다. 일단 HWND를 구하고 나면 그 이후의 구현은 MS에서 제공해 주는 문서대로 구현하면 된다. 해당 문서에 대해서는 참고자료에서 확인할 수 있으니 참조하고, 스마트 클라이언트에서 사용하기 위해 C# 언어로 포팅된 코드는 다음과 같다.

한 가지 더 언급하자면 정상적인 컴파일을 위해 PIA(Primary Interop Assembly) 모듈로 제공되는 ‘Microsoft.mshtml’을 참조 추가해야 한다. 주의해야 할 것은 이 모듈은 ‘닷넷 프레임워크 재배포 모듈’이 설치된 컴퓨터에는 포함되어 있지 않고, VS.NET을 설치한 경우에만 GAC(Global Assembly Cache)에 등록된다는 것이다. 이것으로 인해 문제가 발생하게 되는데, VS.NET 2003 IDE에서 참조를 추가하는 경우 기본적으로 GAC에 등록된 모듈에 대해 로컬 복사가 ‘False’여서 이렇게 되면 클라이언트에서 활성화되는 스마트 클라이언트의 경우 해당 DLL을 찾을 수 없으므로 동작 자체가 되지 않는다. 따라서 반드시 참조로 추가한 다음 해당 모듈의 속성창에서 로컬 복사 속성을 ‘True’로 바꿔야 한다.



// 자신의 윈도우 핸들을 구하고 부모인 WebBrowser 윈도우 핸들을 구한다.
IntPtr pHandle = this.Handle;
pHandle = GetParent( pHandle );

// WebBrowser 컨트롤에서 정의된 특별한 메시지를 얻어낸다.
uint nMsg = RegisterWindowMessage( “WM_HTML_GETOBJECT” );

// ObjectFromLresult의 인자로 전달돼야 할 lRes 값을 구하고
uint lRes = 0;
SendMessageTimeout( pHandle, nMsg, 0, 0, 2, 1000, ref lRes ); // SMTO_ABORTIFHUNG : 2

// MsHTML.h/ComDef.h에서 IHTMLDocument2의 GUID를 얻을 수 있다.
Guid htmlDocumentGuid = new Guid( “332C4425-26CB-11D0-B483-00C04FD90119” );
// MSAA SDK에서 제공하는 함수를 통해
// 마샬링된 IHTMLDocument2 인터페이스 포인터를 구한다.
ObjectFromLresult( lRes, ref htmlDocumentGuid, 0, out _htmlDocument);

Type t = _htmlDocument.GetType();
string title = (string)t.InvokeMember( “title”, BindingFlags.GetProperty, null, _htmlDocument, null );

// mshtml.HTMLDocument docObj = (mshtml.HTMLDocument)obj;
// string title2 = docObj.title;

앞의 코드에서 GetParent, RegisterWindowMessage, Send MessageTimeout, ObjectFromLresult 메쏘드는 Win32 API로서 PInvoke로 호출된다(해당 DllImport 선언과 관계된 소스는 ‘이달의 디스켓’에 실려 있다). 일단은 앞의 과정을 통해 IHTML Document2 인터페이스까지 얻을 수 있다.
앞의 코드를 자세히 보면 맨 마지막 라인에 주석 처리가 된 것에 대해서 의문을 제기할 것이다. 주석 처리된 라인의 위에 보면 IHTMLDocument2::title을 구하기 위해 복잡하게 Type.Invoke Member를 호출한 것을 볼 수 있는데, 굳이 그렇게 한 것에는 ‘보안’과 관계된 설정 사항 때문이다. 이쯤에서 고백하는데, 필자 나름대로 스마트 클라이언트를 개발하면서 이해할 수 없는 현상을 많이 접해 보았다. 앞의 코드 역시 필자에게는 아직 그런 ‘이해할 수 없는 현상’ 중의 하나로 남아 있다.
주석 처리된 코드에서 보는 것처럼 만약 mshtml.HTML Document로 형변환을 하게 되면 지난 호에 설정했던 ‘Ineternet SmartClient_Zone’에 ‘Full Trust’를 줘야 한다. 가능한 최소한의 권한만을 스마트 클라이언트에 주는 것이 바람직하기 때문에 필자로서는 ‘Full Trust’를 사용하지 않는 다른 방법을 찾았고 그것이 Type.InvokeMember였다. 원칙적으로 이해하려고 든다면 mshtml.HTMLDocument를 사용하는 것에는 지난 호에 살펴본 ‘Unmanaged Code 호출 가능’ 권한만으로도 충분하다고 볼 수 있다. 그런데 왜 ‘Full Trust’를 필요로 하게 되었는지에 관해서는 필자 자신도 어떻게 설명할 길이 없다. Full Trust를 스마트 클라이언트에 허용해 mshtml.HTMLDocument를 사용할 것인지, 아니면 제한된 ‘SmartClientSet’을 허용하고 프로그램을 다소 복잡하게 할 것인지는 독자의 선택에 달려 있다. 물론, 필자는 이 연재 전체에 걸쳐서 SmartClientSet을 선택한 것을 기본 전제로 코드를 구현했음을 알아주기 바란다.
앞의 과정을 통해 일단 object 형식의 IHTMLDocument2 인터페이스를 구하게 되었고, Untyped 상태에서 해당 객체의 메쏘드/속성을 호출하는 방법을 알아보았다. 경우에 따라 스마트 클라이언트 제작에 웹 브라우저와 연동을 고려해 볼 때 이 정도 수준에서도 끝낼 수 있다. 사실 액티브X 컨트롤을 제작하던 때를 생각해 보면, IWebBrowser2 인터페이스를 구하려는 목적에는 크게 두 가지를 떠 올릴 수 있을 것이다. 첫 번째는 IWebBrowser2::Document를 통해 컨트롤을 호스팅하고 있는 HTML 문서를 접근하고자 하는 경우이고, 두 번째는 웹 브라우저의 이벤트를 받고 싶은 경우이다. 대개의 경우 첫 번째 목적이라면 ObjectFromLresult 함수의 특이한 능력 덕분에 굳이 IWebBrowser2 인터페이스까지 구할 필요 없이 앞의 코드까지만 구현하면 된다. 하지만 웹 브라우저의 이벤트를 받고자 한다면 결국 IWebBrowser2 인터페이스까지 얻어야 가능하다.
웹 브라우저로부터 이벤트를 받는 것을 목표로 힘들겠지만 한 단계 더 나아가서 IWebBrowser2 인터페이스까지 구해보자. IE에서 액티브X 컨트롤을 만들어 본 독자들은 IWebBrowser2::Document를 통해 IHTMLDocument2 인터페이스를 쉽게 얻었던 기억이 있을 것이다. 하지만 그 역으로 변환하는 것을 해 본 독자들은 많지 않을 것 같다. 일단 그 방법에 관한 C++로 된 소스는 참고자료를 참고하고, 여기서는 C# 버전으로 포팅된 코드로 살펴보자. mshtml.HTML Document를 다루기 위해 Microsoft.mshtml 모듈을 참조한 것과는 달리 IWebBrowser2 인터페이스를 다루기 위한 참조(ShDocVw.dll, “Microsoft Internet Controls”)를 추가할 필요는 없다.

// IHTMLDocument2로부터 IWebBrowser2 인터페이스 얻기

// managed 개체로부터 unmanaged IUnknown 포인터를 반환
IntPtr docPtr = Marshal.GetIUnknownForObject( _htmlDocument );

// IOleCommandTarget GUID는 DocObj.h/ComDef.h에서 구함
Guid ocmTargetGuid = new Guid( “B722BCCB-4E68-101B-A2BC-00AA00404770” );
IntPtr ocmPtr;
// IUnknown::QueryInterface를 Marshal 클래스에 정의된 QueryInterface를 이용
// IHTMLDocument2로부터 IOleCommandTarget 인터페이스를 가져오기
Marshal.QueryInterface( docPtr, ref ocmTargetGuid, out ocmPtr );
Marshal.Release( docPtr );

// IServiceProvider의 GUID는 ServProv.h에서 구함
Guid svpGuid = new Guid( “6d5140c1-7436-11ce-8034-00aa006009fa” );
IntPtr svpPtr;
// IOleCommandTarget으로부터 IServiceProvider 인터페이스를 가져오기
Marshal.QueryInterface( ocmPtr, ref svpGuid, out svpPtr );
Marshal.Release( ocmPtr );

// unmanaged IServiceProvider 개체를 managed 개체로 변환
SmartClient.IServiceProvider svpObject =
(SmartClient.IServiceProvider)Marshal.GetObjectForIUnknown( svpPtr );

// SID_SWebBrowserApp와 IID_IWebBrowser2 GUID는 ExDisp.h에서 구함
Guid sidWBA = new Guid( “0002DF05-0000-0000-C000-000000000046” );
Guid webBrowser2 = new Guid( “D30C1661-CDAF-11d0-8A3E-00C04FC9E26E” );
// IServiceProvider::QueryService를 통해 IWebBrowser2 인터페이스 포인터를 반환
object WBObject = svpObject.QueryService( ref sidWBA, ref webBrowser2 );
Marshal.Release( svpPtr );

// SHDocVw.WebBrowser로 형변환
// 이후 IWebBrowser2 개체와 동일하게 처리
_webBr = (IWebBrowser2)WBObject;

앞의 코드를 보면 unmanaged COM 개체들을 C++로 다루던 것을 managed 환경의 C#에서 다루는 것이 쉽지만은 않아 보인다. 앞의 내용에서 핵심 작업은 System.Runtime.InteropServices. Marshal 클래스로 어떻게 unmanaged 환경에서 COM 작업을 대체하느냐 하는 것인데, 보는 것처럼 C/C+보다 약간 부가적인 코드를 필요로 해도 C#으로도 동일하게 포팅할 수 있다. COM 개체와 상호운용을 가능케 하는 Marshal 클래스에는 앞의 코드에서 보여준 메쏘드 이외에도 unmanaged와 managed 사이의 다리 역할을 해주는 static 함수들이 상당수 포함되어 있으니 시간이 날 때 틈틈이 살펴보는 것도 좋겠다.
웬만큼 COM을 경험한 독자들의 경우에는 앞의 코드를 하나씩 뜯어보면 모르는 부분은 없을텐데, 그래도 IServiceProvider에 대해서는 짚고 넘어가야겠다. 그 앞의 코드에서 본 것처럼 IDispatch를 상속받은 인터페이스의 메쏘드 호출은 Type.InvokeMember를 통해 가능하다는 것을 알았다. 하지만 IUnknown으로부터 상속받은 IServiceProvider 같은 경우에는 사정이 달라진다. 즉 DISPID가 없는 경우에는 직접 vtable에 기반한 메쏘드 호출을 해야 하기 때문이다. 다행히도 C#의 interface 구문으로 다음과 같이 C/C++에서와 같은 interface를 만들 수 있게 해주고 있다. 더군다나 IService Provider 같은 경우에는 단순하게 IUnknown 인터페이스에서 단 하나의 QueryService 메쏘드만을 추가한 인터페이스이기 때문에 그다지 어렵지 않게 구현할 수 있다.

[Guid(“6d5140c1-7436-11ce-8034-00aa006009fa”)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] // IUnknown 기본 구현
[ComImport()]
public interface IServiceProvider
{
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryService( ref Guid guidService, ref Guid riid );
}

물론 앞의 코드에 구현된 인터페이스를 직접 상속받아서 사용할 일은 없다. unmanaged 환경에서 구한 인터페이스 포인터를 managed 개체로 변환한 후에 스텁(stub)만 구현해 둔 인터페이스로 ‘형변환’을 해주어 사용하는 목적으로 쓰일 것이기 때문이다. 사실 IServiceProvider까지는 구현이 간단한데, 그와 동일하게 여러분은 IWebBrowser2에 대해서도 앞의 코드와 같은 형태의 인터페이스를 직접 코딩해 줘야 한다. 만약 이벤트까지 생각한다면 DWebBrowser Events2 인터페이스까지도 만들어야 한다. 그에 대해 어떤 독자들은 왜 SHDocVw.dll을 참조한 RCW(Runtime Callable Wrapper)를 사용하지 않느냐고 말할지도 모르겠다.
이론상으로는 분명 SHDocVw.dll을 참조 추가하는 것으로 끝나야 정상이다. 그러나 현실은 그렇지 않다. 앞에서 IWebBrowser2를 IServiceProvider처럼 인터페이스 정의로 하지 않고 직접 SHDocVw.dll을 참조해서 형변환하여 쓰게 되면 스마트 클라이언트가 실행시에 오류를 발생하고 만다. 필자는 그런 이해할 수 없는 현상 앞에서 많은 시간을 소비했고, 결국 찾아낸 것이 C# 인터페이스를 별도로 정의하는 것이었다. 참고로 IWebBrowser2 인터페이스와 DWebBrowserEvents 인터페이스 정의는 ‘이달의 디스켓’으로 제공하니 참조하기 바란다.