Search

'프로그래밍'에 해당되는 글 45건

  1. 2012.08.27 Detours Hooking Framework - 3-3. Hooking virtual function
  2. 2012.02.17 boost, io_service 그리고 io_service.run() (2)
  3. 2011.07.29 Detours Hooking Framework - 3-2. Hooking class member function (6)
  4. 2011.07.15 Detours Hooking Framework - 3-1. Hooking class member function
  5. 2011.07.12 Detours Hooking Framework -2. Hooking API (3)
  6. 2011.06.16 Detours Hooking Framework - 1. hooking function (13)
  7. 2011.04.10 DataGrid and Dynamic Column -- Part2
  8. 2011.04.01 DataGrid and Dynamic Column -- Part1 (2)
  9. 2011.03.23 GenericTypeExtension works in .NET4.0
  10. 2011.03.14 WPF UI Virtualization -2, SeeMoreStackPanel 만들기
  11. 2011.03.01 WPF UI Virtualization -1
  12. 2011.01.09 언제부터 보이지 않는 키워드 for, foreach. (8)
  13. 2010.10.03 WPF를 생각한다2 - 좋은 프레임워크의 조건. (2)
  14. 2010.08.11 WPF를 생각한다. (6)
  15. 2010.07.15 Canvas supports ScrollViewer.
  16. 2010.07.14 TwoWay Binding of SelectedItem in TreeView
  17. 2010.05.31 Left Shift + Right Shift + 마우스 클릭
  18. 2010.03.29 Simple WPF Tip : Is A in bound of B?
  19. 2010.03.10 Social MSDN Forum 활동
  20. 2010.03.08 Simple Tip -- ResourceDictionary동적 생성을 위한 x:Class의 활용
  21. 2010.02.25 Using a ViewModel as a value converter
  22. 2010.02.23 LambdaValueConverter
  23. 2010.02.09 WPF Template의 이해
  24. 2009.12.14 Code Contract -2
  25. 2009.12.09 Beyond the Assertion, Code Contract -1
  26. 2009.11.30 Visual Studio Solution File(.sln) Parser
  27. 2009.11.11 WPF Dialog 컨트롤들
  28. 2009.11.04 한글 오토마타 (4)
  29. 2009.08.26 DependencyProperty Value Inheritance (3)
  30. 2009.06.09 Floating Control Arpa (2)

아이고. 이 글을 이제야 쓰게 되었습니다. 죄송합니다. 그동안 이 주제에 대해 개인적으로 물어보시는 분들에게는 메일로 답변 드렸는데, 답변 내용을 여기에도 올립니다.

 

가상함수 후킹과 COM 후킹은 근본적으로 동일하니 같은 방식을 사용하시면 됩니다.

 

Detours로 후킹 할 때 가장 중요한 것은 후킹의 대상이 되는 Function의 주소를 찾는 것입니다. 그런데 아시다시피 가상함수의 주소는 일반적인 방법으로 알 수 없습니다.

 

직접 프로그램을 만들어 디버깅 한 후 가상함수와 Object간의 Offset을 알아내야 해당 가상 함수의 Real주소를 알게 됩니다.

 

Real주소를 알았다면 Detours로 후킹하는 것은 간단하지요.

 

 

아래에서 큰 글자로 표시된 부분을 살펴보시기 바랍니다. Pvtbl 은 가상함수 테이블의 주소이고, -25888 값은Offset입니다.

 

 Offset을 확인하려면 한번 가상함수를 디버깅해 해당 주소를 알아 낸 후 해당 주소 – pvtbl 을 계산하면 됩니다.

 

COM Object또는 가상 함수를 가지는 객체가 변경되지 않았다면 이 Offset값은 항상 고정입니다.

 

#include <stdio.h>

#include <Windows.h>

#include <detours.h>

static PDETOUR_TRAMPOLINE Trampoline;

 

 

class CMember{

public:

           void Target(void);

};

 

void CMember::Target(void){

           printf("Cmember :: Target! (this:%p)\n"this);

 

}

 

class CDetour{

public :

           void WINAPI Mine_Target(CHAR* string);

           static void (CDetour::*Real_Target)(void);

};

 

void CDetour::Mine_Target(CHAR* string){

           printf("  CDetour::Mine_Target! (this:%p)\n"this);

           //__asm push edx;

           __asm mov eax, [esp];

           __asm push eax;

           __asm push eax;

           __asm call [Trampoline];

           //void (WINAPI *OriginalTarget)(CHAR* ) = (void (WINAPI *)(CHAR*))((PBYTE)Trampoline);

           //OriginalTarget(string);

}

 

void (CDetour::*CDetour::Real_Target)(void) = (void (CDetour::*)(void))&CMember::Target;

 

void WINAPI testMethod(CHAR* test){

           printf("general method %s\n",test);

}

 


 

class IInterface{

public :

           virtual void WINAPI test(CHAR* string)=0;

};

 

class ImplementClass : public IInterface{

public :

           void WINAPI test(CHAR* string){

                     printf("test test");

           }

};

 

 

 

int main(intchar**){

           LONG error;

           IInterface* inter;

           ImplementClass imple;

           inter = &imple;

       void* pvtbl;

       memcpy(&pvtbl, inter, sizeof(pvtbl));

    DetourTransactionBegin();

    DetourUpdateThread(GetCurrentThread());

       PBYTE tmp = (PBYTE)pvtbl - 25888;

           PVOID DetourPtr;

           PVOID TargetPtr;

       

           testMethod("asdfadsf");

         

           inter->test("test -- before detourAttach");

          

           DetourAttachEx(&(PVOID&)(tmp),(PVOID)(&(PVOID&)CDetour::Mine_Target), &Trampoline, &TargetPtr, &DetourPtr);

           error = DetourTransactionCommit();

          

 

           inter->test("test----inner virtual function");

 

    if (error == NO_ERROR) {

        printf("\n");

    }

           else{

                     printf("fail to attach\n");

           }


 

 

           getchar();

           return 0;

}

저작자 표시
신고

boost, io_service 그리고 io_service.run()

프로그래밍 2012.02.17 22:09 Posted by 아일레프

boost, 엄청 훌륭하다. 정말 좋다. C++의 놀라움은 C#의 Linq와 같은 기능을 추가적인 프로그램 언어 문법이 없어도 (lambda와 같은) meta programing 으로 구현할 수 있다는 사실에 있다. 물론 Template 코드인 특정 함수가 Parameter로 어떤 Type을 원하는지 알기란 나같은 초짜로서는 너무 버겁다. 하지만 이때 탓해야 할 것은 부족한 나. boost그리고 그것이 가능하게 하는 c++언어는 눈물 나도록 아름답고 내가 완전히 그것을 정복하기란 불가능해 보일 정도로 높은 위치에 있다. 엉엉엉

여기까진 순전히 사족이고, 본론을 시작한다. 요즘 TCP/IP Socket 프로그래밍 할 일이 계속 생겨서 이 boost를 계속 활용하고 있는데, 정말 어이 없는 실수로 4시간을 소비했다. 이것이 실수인지 판단하기 위해 얼마나 많은 시간을 허비했는지. 엉엉엉.

io_service는 Thread Pool과 비슷한 역할을 한다. boost::asio::ip::tcp::socket은 이런 io_service의 성질을 이용해 async_write, async_read를 구현할 수 있다. 자, 그럼 내가 고생했던 사항을 여기 간단하게 고쳐 적어본다.

boost::asio::io_service io_service;
TcpIpClientManager manager(io_service);
boost::thread thread(boost::bind(&boost::asio::io_service::run, &io_service));
io_service.post(boost::bind(&MainStarter::PostProcessUsingIoService, this));

자, 위 코드의 의도는 알 것이다. TcpIpClientManager라는 객체는 TCP/IP통신을 위해 만든 객체이다. 내부에 private boost:asio::ip::tcp::socket을 가지고 있다.

io_service.run()을 호출하면 주 thread가 Block되므로 thread로 돌렸다. 그리고 PostProcessUsingIoService라는 함수를 io_service에 맡겨 실행되게 했다.

자, 어떤 일이 벌어질까? 이 코드의 문제점은 무엇일까? 이 경우 PostProcessUsingIoService 함수는 실행 될 수도 있고, 실행되지 않을 수도 있다. 무슨 말인고 하니, TcpIpClientManager 내부에서 io_service를 사용해 어떤 활동을 하는 코드가 실행 중이라면 저 코드는 실행된다. 그런데 만약 io_service를 사용하는 모든 일들이 종료 되었다면 저 코드는 실행되지 않는다. io_service가 run 상태가 아니기 때문이다.

이것은 io_service문서를 조금만 주의 깊게 보면 알 수 있다.

Some applications may need to prevent an io_service object's run() call from returning when there is no more work to do. For example, the io_service may be being run in a background thread that is launched prior to the application's asynchronous operations. The run() call may be kept running by creating an object of type io_service::work:

젠장. 왜 document를 보지 않고 예제만 보고 프로그래밍을 했기에 이와 같은 멍청한 일이 생겼다. 나를 탓할 수 밖에 없다. 그래서, PostProcessUsingIoService가 t실행 되는 것을 항상 보장 하려면 - 즉 io_service가 항상 run상태이게 하려면 - 다음과 같이 work를 사용해야 한다.

boost::asio::io_service io_service;
boost::asio::io_service::work work(io_service);
TcpIpClientManager manager(io_service);
boost::thread thread(boost::bind(&boost::asio::io_service::run, &io_service));
io_service.post(boost::bind(&MainStarter::PostProcessUsingIoService, this));

결론.

Document는 졸라 중요합니다. 당신의 소중한 시간을 낭비하지 않기 위한 최선의 방법은 당장 구현된 예제를 보는 것이 아니라 Document를 보는 것입니다.

신고

이전 글

Detours Hooking Framework 1 - Hooking Function

Detours Hooking Framework 2 - Hooking API

Detours Hooking Framework 3-1 Hooking class member function

 

잠시 복습

지난 글에서 class member function을 호출했을 때 컴파일 된 assembly에 대해 살펴보았다. 나머지는 전부 제쳐두고 다음을 기억하면 됐다.


    int result1 = a.TestFunction(10);
008E1612  push        0Ah  
008E1614  lea         ecx,[a]  
008E1617  call        TestClass::TestFunction (8E111Dh)  

class의 instance의 주소를 ecx에 넣고 해당 함수를 콜 한다는 규칙이었다. 자, 이것을 기억해두고 class member function을 후킹해보자.

 

 

Hooking class member function


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;

public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    //여기서­ 진짜 멤버 함수를 호출해야한다.
    return 0;
}
int main(int argc, char** argv){
    TestClass a(10),b(20);

    int result1 = a.TestFunction(10);

    return 0;
}


위 코드에서 TestClass의 TestFunction함수를 후킹해보자. Detours를 사용하기 위해 TestClass::TestFunction과 HookingFunction의 address를 넘겨야 한다. 이때 main()함수의 코드는 다음과 같게 된다.


int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    //여기서 진짜 멤버 함수를 호출해야 한다.
    return 0;
}

PVOID functionAddress ;

int main(int argc, char** argv){
    TestClass a(10),b(20);
    functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&functionAddress, HookingFunction);
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}

위 작업으로 a.TestFunction(10)을 실행했을 때 HookingFunction이 호출되게 된다. 기존 TestClass::TestFunction이 존재하던 Address에 jmp HookingFunction이란 코드가 대치되었기 때문이다. 그런데 HookingFunction코드 내에서 어떻게 TestClass::TestFunction을 실행할 수 있을까?

 

TestClass::TestFunction 을 실행하기 위한 조건은 다음과 같았다. 먼저 TestClass의 instance, a의 주소를 ecx에 넘겨 주어야 하고 TestClass::TestFunction의 address를 call해야 한다. 그런데 문제가 있다. a의 주소를 HookingFunction 내에서 알아낼 방법이 없다는 것이다. a의 주소는 ecx로 넘어 오기 때문에 알 수 있지 않냐고 생각할 수 있겠지만 ecx를 일반함수 HookingFunction에서 보존한다는 보장이 없기 때문이다. 만약 ecx값을 보존한다면 다음과 같이 코딩하면 되겠지만 이는 실패한다.


int HookingFunction(int value){
    std::cout<<"Hooked" <<std::endl;
    int result;
    __asm push value;
    __asm call [functionAddress];
    __asm mov [result], eax;
    return result;
}


그럼 어떻게 해야할까? 답은 의외로 간단하다. 일반 함수로 Hooking하는 것이 아니라 클래스의 멤버함수로 Hooking하면 된다. 다음과 같이 말이다.


class HookingClass{
public:
    int HookingFunction(int value){
        std::cout<<"Hooked" <<std::endl;
        int result;
        __asm push value;
        __asm mov ecx, [this];
        __asm call [functionAddress];
        __asm mov [result], eax;
        return result;
    }
};


int main(int argc, char** argv){
    TestClass a(10),b(20);
    functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&functionAddress, (PVOID)(&(PVOID&)HookingClass::HookingFunction));
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}


위 코드를 실행시키면 HookingClass::HookingFunction(int value)내에서 TestClass::TestFunction을 실행할 수 있음을 알 수 있다. __asm mov ecx, [this];  에서 [this]가 곧 instance a의 주소이다. 이때 __asm으로 어셈블리 코드를 직접 사용하는 것이 싫으면 다음과 같이 하면 된다.


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;

public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

class HookingClass{
public:
    int HookingFunction(int value);
    static int (HookingClass::*Real_Target)(int);
};

int HookingClass::HookingFunction(int value)
{
    std::cout<<"Hooked" <<std::endl;
    return (this->*Real_Target)(value);
}
int (HookingClass::* HookingClass::Real_Target)(int) = (int (HookingClass::*)(int))&TestClass::TestFunction;

int main(int argc, char** argv){
    TestClass a(10),b(20);

    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&(PVOID&)HookingClass::Real_Target, (PVOID)(&(PVOID&)HookingClass::HookingFunction));
    
    error = DetourTransactionCommit();

    int result1 = a.TestFunction(10);

    return 0;
}


굉장히 아름다운 이 방법은 detours의 sample에 포함된 소스코드를 참조한 것이다. 붉은 색으로 된 코드를 보고 그 안에 담긴 의미를 음미해보라. 다음 마지막이 될 다음 포스트에는 virtual function을 후킹하는 법에 대하 알아보도록 한다.

 

신고

이전 글

Detours Hooking Framework 1 - Hooking Function

Detours Hooking Framework 2 - Hooking API

 

잠시 복습

첫번째 Detours Hooking Framework 글에선 일반 Function을 Hooking하는 법을 살펴보았고 두번째 글에서는 Detours로 API를 후킹하는 법에 대해 알아보았다. Detours가 훌륭한 것은 동일한 방식으로 다른 분류의 Hooking을 할 수 있다는 사실이다. 사용자의 입장에선 내부적으로 어떤 일이 벌어지는지 신경쓸 필요가 없다. 조금만 신경쓰면 된다. 잠시 이전에 Detours가 어떻게 Hooking을 했는지 복습해보자. 일반 함수의 경우 함수 심볼 위치에 있는 명령어를 사용자가 원하는 명령어로 변경해줌으로써 이 일이 가능했다.

위를 보면 TestFunction: 심볼 값에 위치한 명령이 jmp TestFunction에서 jmp HookingFunction으로 변경된 것을 확인할 수 있다. 또한 Dynamic Link된 API를 Hooking할 때는 좀더 복잡한 Code Patch방식을 사용한다는 것이 확인되었다. 만약 해당 API Function의 주소가 A라면 해당 주소에 jmp HookingFunction이라는 명령어가 들어가는 식이었다. 하지만 근본적인 방식은 특정 메모리 주소에 Jmp HookingFunction이라는 명령어를 쓰는 방식으로 동일했다. 그리고 변경되기 전 메모리 주소의 명령어들은 다른 메모리 주소로 옮겨 진다. 이것은 현대의 컴퓨터가 폰노이만 머신, 즉 Code와 Data가 동시에 같은 메모리상에 존재하기 때문에 가능한 것이었다.(VirtualProtect함수로 해당 메모리 주소에 WRITE속성을 추가하고 memcpy로 명령어를 쓰는 것으로 이루어진다.) 당연히 후킹을 위해선 반드시 후킹하고 싶은 함수 또는 API의 메모리 주소를 알아야한다.

 

Class member function call을 디벼보자.

class member function을 후킹하려고 한다. 먼저 후킹이 가능하려면 해당 member function의 주소를 알아내야 한다. 그런데 그것만으로 될까?


#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <detours.h>

class TestClass{
private :
    int mInt;
public :
    TestClass() : mInt(0){}
    TestClass(int initValue) : mInt(initValue){}
    int TestFunction(int iValue);
};

int TestClass::TestFunction(int iValue){
    return mInt + iValue;
}

int main(int argc, char** argv){
    TestClass a(10),b(20);
    int result1 = a.TestFunction(10);
    int result2 = b.TestFunction(10);
    return 0;
}


위와 같은 초 단순한 예를 한번 생각해보자. 후킹을 하기 위해서 해당 함수의 메모리 주소를 알아야한다. 그렇다면 여기서 a.TestFunction 함수와 b.TestFunction 함수의 메모리 주소는 같을까 다를까? 같다면 하나의 작업으로 여러 Instance의 TestFunction을 후킹할 수 있을 것이고 다르다면 Instance마다 TestFunction을 후킹해야한다. 이를 확인하기 위해 a.TestFunction 함수 콜과 b.TestFunction 함수 콜이 Assembly로 어떻게 컴파일 되는지 살펴보자.


    int result1 = a.TestFunction(10);
008E1612  push        0Ah  
008E1614  lea         ecx,[a]  
008E1617  call        TestClass::TestFunction (8E111Dh)  
008E161C  mov         dword ptr [result1],eax  
    int result2 = b.TestFunction(10);
008E161F  push        0Ah  
008E1621  lea         ecx,[b]  
008E1624  call        TestClass::TestFunction (8E111Dh)  
008E1629  mov         dword ptr [result2],eax  


위 어셈블리를 통해 알 수 있듯이 a.TestFunction 함수와 b.TestFunction은 같은 주소 값을 가진다. 이것은 동일한 클래스의 Instance가 여러개 존재하더라도 해당 클래스의 Function은 같은 메모리 주소에 있다는 것을 의미한다. 그렇다면 해당 클래스 멤버 함수는 자신이 어떤 Instance에 속해 실행되는지 어떻게 알 수 있을까? 옳지, call 명령 전의 lea ecx, [a]에 해답이 있을 것 같다. lea ecx, [a]명령이 완료되면 a변수의 주소가 ECX 레지스터에 들어가게 된다. 아하, TestClass::TestFunction은 이 ECX 레지스터에 있는 a의 주소를 통해 정확한 mInt의 값을 얻어 내는 것이구나! a.TestFunction(10)으로 들어가 확인해보자.


int TestClass::TestFunction(int iValue){
008E1590  push        ebp      //Stack Frame을 사용하기 위해 ebp를 Stack에 저장한다. 
008E1591  mov         ebp,esp  //현 esp 값을 ebp에 저장한다. 
008E1593  sub         esp,0CCh //함수내에 사용될 스택의 크기를 0x0CC만큼 확보한다. 
008E1599  push        ebx      //ebx 레지스터의 값을 스택에 저장한다. 
008E159A  push        esi      //esi 레지스터의 값을 스택에 저장한다. 
008E159B  push        edi      //edi 레지스터의 값을 스택에 저장한다. 
008E159C  push        ecx      //ecx 레지스터의 값을 스택에 저장한다. 
008E159D  lea         edi,[ebp-0CCh]  //여기서 부터 rep stos까지의 명령은 하나의 명령 set으로 보면된다. 
008E15A3  mov         ecx,33h         //ecx에 0x33 값을 넣는다.  
008E15A8  mov         eax,0CCCCCCCCh  //eax에 0xcccccccc값을 넣는다. 
008E15AD  rep stos    dword ptr es:[edi]  
//rep stos dword ptr es:[edi]는 edi, ecx, eax와 연관된다. 
//보통 함수의 첫 시작에 불리는 이 명령어 집합은 스택에 초기값을 넣는 것에 활용된다. 
//이 명령은 edi의 주소 값에 eax값을 넣는다. 그리고 그 다음에는 edi+4의 주소 값에 eax값을 넣는다. 
//다음에는 edi+8의 주소 값에 eax값을 넣는다. 이것을 총 ecx내의 수 만큼 반복한다. 즉 0x33번 반복한다.(0x33*4 = 0xcc)
008E15AF  pop         ecx  //ecx값을 복구한다.
008E15B0  mov         dword ptr [ebp-8],ecx  //ecx값을 ebp-8의 주소에 넣는다. 
    return mInt + iValue;
008E15B3  mov         eax,dword ptr [this] //this 주소 값을 eax에 넣는다.   
008E15B6  mov         eax,dword ptr [eax]  //eax의 주소의 value값을 eax에 넣는다. 
008E15B8  add         eax,dword ptr [iValue]  //eax에 iValue값을 더해 넣는다. 
}
008E15BB  pop         edi  //edi값 복구  
008E15BC  pop         esi  //esi값 복구
008E15BD  pop         ebx  //ebx값 복구
008E15BE  mov         esp,ebp  //esp = ebp
008E15C0  pop         ebp  //ebp값 복구
008E15C1  ret         4  //esp = esp +4, 함수 호출했던 주소로 가기 위해 또한번 pop. 


 

Stack

음... Stack관련 명령만 대해 먼저 정리하자. 먼저 알아야 할 것은 함수가 불리기 전의 ESP값과 함수가 불린 후의 ESP 값이 동일해야 한다는 사실이다. 그리고 Stack은 함수 내 변수 공간을 위해 활용된다. 이 Stack의 변화를 보기 위해 Stack관련 명령어들만 정리한 다음 그림을 보기 바란다.

이 과정으로 ESP가 함수 호출이 끝나면서 자연스럽게 복귀됨을 알 수 있다. 이 assembly와 같이 EBP 레지스터를 이용해 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 Stack Frame기법이라 한다. 보통 이 과정은

push EBP
mov EBP, ESP
...
mov ESP, EBP, 
pop EBP
ret 

로 압축된다. 이 stack frame을 이용하면 함수 호출 깊이가 깊어져도 스택을 완벽하게 관리할 수 있다. 더 관심있으신 분들은 검색해보시길~ 

 

 

ECX, 그리고 EBP-8 == this

하여간 여기서 중요한 것은 Stack Frame이 어떻게 동작하는가가 아니다. 우리가 궁금한 것은 어떻게 TestClass::TestFunction이 자신의 Instance의 mInt변수값을 알아 낼 수 있는가였다. 이 부분에 해당하는 코드만 따로 보자.


main함수 내에서

    int result1 = a.TestFunction(10);
01041612  push        0Ah  
01041614  lea         ecx,[a]  
01041617  call        TestClass::TestFunction (104111Dh)  
0104161C  mov         dword ptr [result1],eax  

 

TestClass::TestFunction 내에서

010415B0  mov         dword ptr [ebp-8],ecx  
    return mInt + iValue;
010415B3  mov         eax,dword ptr [this]  
010415B6  mov         eax,dword ptr [eax]  
010415B8  add         eax,dword ptr [iValue]  
}


위 부분이 궁금한 부분을 압축한 코드라 할 수 있다. 보자. 함수 호출전에 ecx값에 a변수의 주소가 들어갔었다. TestFunction내에서 ebp-8의 주소에 ecx값을 넣고 있다. 이때 ebp 에는 -12값이 들어있으므로 -20에 ecx값, 곧 a의 변수 값을 넣고 있다. 이 다음에 나오는 코드가 mov eax, dword ptr [this]이다. 이것은 무엇일까?

놀랍게도 이것은 mov eax, dword [ebp-8]과 동일한 의미를 지닌다. 중요한 포인트인데, 바로 클래스 멤버 함수의 ebp-8에는 해당 클래스의 instance의 주소가 들어가게 된다는 사실이다. 이것은 일종의 약속이다. 그리고 해당 instance의 주소에는 클래스 멤버 변수들의 값이 들어있다. 이 경우 해당 instance의 주소에 mInt값이 들어있으므로 mov eax, dword ptr[eax]로 해당 멤버 변수의 값을 eax에 로드할 수 있는 것이다. 그렇다면 만약 TestClass에 int mInt2라는 변수가 추가되어있다면 어떨까? 그리고 이 변수는 어떻게 클래스 함수에서 접근할까? 딩동댕! 다음과 같은 꼴일 것이다.

mov eax, dword ptr[this+4]

 

 

Class Member Function Pointer

비밀들이 벗겨지고 있다. 위에서 설명한 이유 때문에 일반 Function Pointer로 함수 멤버 함수를 호출할 수 없는 것이다. 예를 들어 TestClass::TestFunction의 주소를 얻어와 이것을 일반 Function Pointer로 만든 후 해당 Function Pointer를 호출해보자.


TestClass a(10),b(20);
typedef int (*FPointer)(int value);
PVOID functionAddress = (PVOID)(&(PVOID&)TestClass::TestFunction);
FPointer pointer = (FPointer)functionAddress;
pointer(10);


위 코드는 정상적으로 빌드가 되고 pointer에 TestClass::TestFunction의 주소 값이 정상적으로 들어가지만 Run-time Exception을 발생시킨다. 그리고 이것은 너무나 당연한 결과이다.


00B41621  push        0Ah  
00B41623  call        dword ptr [pointer]  


class의 Instance를 넘겨주는 명령이 빠져있는 것을 볼 수 있다. 이와 반대로 Class Member Function Pointer를 활용해보자.


typedef int (TestClass::*PFClassMember)(int);
PFClassMember classMemberFP = &TestClass::TestFunction;
(a.*classMemberFP)(10);


    int result3 = (a.*classMemberFP)(10);
0107161B  push        0Ah  
0107161D  lea         ecx,[a]  
01071620  call        dword ptr [classMemberFP]  

ecx에 변수 instance a의 주소값을 넘겨주고 있다. 이로써 해당 함수 콜이 정상적으로 동작할 수 있다.

 

 

결론

일반 함수 콜과 클래스 멤버 함수 콜이 어떻게 다른지 기초적인 부분을 알아보았다. 다음 포스트에서는 이 TestFunction 클래스 함수를 후킹해보자! 

 

**혹시 이 포스트에서 제가 잘못 알고 있는 부분이 있다면 꼭 태클해주세요. 감사합니다.

신고

Detours Hooking Framework -2. Hooking API

프로그래밍 2011.07.12 16:44 Posted by 아일레프

지난 글에서 Detours로 Function을 후킹하는 방법을 살펴보았다. 이 포스트에는 API를 Hooking하는 법을 알아본다. 물론, 매우 간단하고 쉽다.

 

API Hooking

API를 후킹하는 방법은 지난 글에서 말했던 방식과 완전히 동일하다. 단, 내부에서 Detours가 Hooking하는 방식은 다르다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

int main(int, char **){
    ::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);
}

위 코드에서 MessageBoxA를 후킹해 MessageBox 창의 caption을 "Hooked"로 바꿔보자. 코드는 다음과 같을 것이다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

int (WINAPI *pfTrueMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType) = MessageBoxA;

int WINAPI HookingMessageBox(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType){
        
        return pfTrueMessageBox(hWnd, lpText, "Hooked", uType);
}

int main(int, char **){

    DWORD error;
    ::DetourTransactionBegin();
    ::DetourUpdateThread(::GetCurrentThread());
    ::DetourAttach((PVOID*)&pfTrueMessageBox, HookingMessageBox);
    error = DetourTransactionCommit();

    if(error != NO_ERROR){
        printf("fail to attach\n");
    }

    ::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);

}

이 프로그램을 실행시키면 다음 결과를 확인할 수 있다.

Caption이 바뀌었음을 확인 할 수 있다. 자, 이제 내부에서 어떤 일이 벌어졌는지 확인해보자.

먼저 Hooking되기 이전의 MessageBoxA 함수를 호출하는 명령은 다음과 같다.

::MessageBoxA(NULL, "messageBox", "Hello", MB_OK);
00EC165C  mov         esi,esp 
00EC165E  push        0 
00EC1660  push        offset string "Hello" (0EC7854h) 
00EC1665  push        offset string "messageBox" (0EC7844h) 
00EC166A  push        0 
00EC166C  call        dword ptr [__imp__MessageBoxA@16 (0ECC404h)] 
00EC1672  cmp         esi,esp 
00EC1674  call        @ILT+445(__RTC_CheckEsp) (0EC11C2h) 

명령어를 통해 알 수 있듯이 __imp__MessageBoxA의 심볼 값 - 0xECC404h주소의 value 를 call하라는 명령어 이다. 그리고 0xECC404h주소에는 0x7670fd1e 값이 들어있다.

 

그리고 해당 주소에는 MessageBoxA 함수의 코드가 위치해 있다.

7670FD1E  mov         edi,edi 
7670FD20  push        ebp 
7670FD21  mov         ebp,esp 
7670FD23  push        0 
7670FD25  push        dword ptr [ebp+14h] 
7670FD28  push        dword ptr [ebp+10h] 
7670FD2B  push        dword ptr [ebp+0Ch] 
7670FD2E  push        dword ptr [ebp+8] 
7670FD31  call        7670FCD6 

또한 MessageBoxA의 function Pointer를 저장한 pfTrueMessageBox 의 값은 0x7670FD1E이다.

코드를 진행해 Detours로 Attach한 뒤의 Assembly 명령어를 살펴보면 다음과 같은 변화가 있다는 것을 알 수 있다.

7670FD1E  jmp         HookingMessageBox (0EC12BCh) 
7670FD23  push        0 
7670FD25  push        dword ptr [ebp+14h] 
7670FD28  push        dword ptr [ebp+10h] 
7670FD2B  push        dword ptr [ebp+0Ch] 
7670FD2E  push        dword ptr [ebp+8] 
7670FD31  call        7670FCD6 

먼저 위와 같이 0x7670FD1E주소의 명령어가 변경되었다. 그리고, pfTrueMessageBox의 값이 0x6fff0060으로 변경되었다.

그리고 0x6fff0060의 위치에는 다음 명령어가 존재함을 확인 할 수 있다.

6FFF0060  mov         edi,edi 
6FFF0062  push        ebp 
6FFF0063  mov         ebp,esp 
6FFF0065  jmp         7670FD23

6FFF0060 - 6FFF0063은 Detour로 Attach하기 전 0x7670FD1E - 0x7670FD21의 명령어였다.  

정리해보자. 이전 포스트에서 Detours는 단순히 함수 심볼 값의 명령어를 변경할 뿐이었다. 하지만 이번에는 새로운 명령어가 기존의 위치에 추가되고, 기존의 명령어가 다른 위치로 이동됨으로써 Hooking 동작이 이루어졌다. 물론 다른 방식으로 이 API후킹이 이루어질 수도 있다. 0xECC404h 주소의 값에 HookingMessageBox 주소(0x0EC12BCh)를 적어 놓는 것도 하나의 방법일 것이다. 하지만 Detour는 기존의 코드를 변경하는 방식을 택했다는 것을 확인해주기 바란다. 만약 Detours를 통하지 않고 직접 API를 후킹하고 싶다면 www.reversecore.com/63을 확인하면 된다. 정말 자세히 잘 설명 되어있다. 다음에는LoadLibrary, GetProcAddress를 통해 얻어진 API를 후킹하는 법에 대해 살펴볼 것이다.

 

 

** 2011. 07. 12 추가 내용

LoadLibrary, GetProcAddress로 얻어진 함수를 후킹하는 방법을 새 글로 다루려고 했는데 사실 이것은 이 포스트의 내용과 다르지 않기에 여기에 추가한다.

위에서 MessageBoxA함수를 호출할 수 있는 것은 User32.dll이 동적 링크 되었기 때문이다. 그런데 User32.dll을 동적 링크하라는 명령이 보다시피 프로그램내에서 존재하지 않고 있다. 이는 User32.dll이 암시적으로 동적로딩되었기 때문이다. (PE Loader가 프로그램이 시작할 때 이 일을 한다.) 하지만 때때로 프로그래머는 명시적으로 dll을 로딩하고 해당 dll의 exported된 함수를 사용하고 싶을 때가 있는데 이때 사용하는 명령어가 LoadLibrary, GetProcAddress이다. 위 프로그램과 같은 프로그램을 만들되, 명시적 Dynamic Link를 사용해보자. 프로그램은 다음과 같을 것이다.


typedef int (WINAPI *PFMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType);

PFMessageBox pfTrueMessageBox;
int main(int, char **){
    HMODULE user32Module = LoadLibrary(TEXT("user32.dll"));

PFMessageBox messageBox = (PFMessageBox)::GetProcAddress(user32Module,
"MessageBoxA"); messageBox(NULL, "messageBox", "Hello", MB_OK); }


위 프로그램을 Detours로 후킹하면 다음과 같다.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

typedef int (WINAPI *PFMessageBox)(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType);

PFMessageBox pfTrueMessageBox;

int WINAPI HookingMessageBox(
    __in_opt HWND hWnd,
    __in_opt LPCSTR lpText,
    __in_opt LPCSTR lpCaption,
    __in UINT uType){
        
        return pfTrueMessageBox(hWnd, lpText, "Hooked", uType);
}

int main(int, char **){
    HMODULE user32Module = LoadLibrary(TEXT("user32.dll"));
    pfTrueMessageBox = (PFMessageBox)::GetProcAddress(user32Module, "MessageBoxA");
    DWORD error;
    ::DetourTransactionBegin();
    ::DetourUpdateThread(::GetCurrentThread());
    ::DetourAttach((PVOID*)&pfTrueMessageBox, HookingMessageBox);
    error = DetourTransactionCommit();

    if(error != NO_ERROR){
        printf("fail to attach\n");
    }
    
    PFMessageBox messageBox = (PFMessageBox)::GetProcAddress(user32Module, "MessageBoxA");
    messageBox(NULL, "messageBox", "Hello", MB_OK);

}


pfTrueMessageBox와 messageBox 함수 포인터가 가리키는 주소가 다르다는 것에 주의해야 한다. pfTrueMessageBox에는 기존의 MessageBoxA를 호출하는 명령 있으며 messageBox에는 HookingMessageBox를 호출하는 명령이 있다. 그리고 이 일을 하기 위해 Detours가 하는 일은 암시적 동적 링크된 API를 후킹하는 방식과 완전히 동일하다.

신고

Detours Hooking Framework - 1. hooking function

프로그래밍 2011.06.16 00:37 Posted by 아일레프

최근 Hooking을 해야 할 일이 생겼다. 그런데 이 Hooking할 함수가 dll의 일반 export함수도 아니고, 클래스 멤버의 함수도 아니고, COM객체의 virtual 함수라 이리저리 많은 삽질을 해야했다. 그러다 Detours란 Hooking Framework를 만나게 되었는데 와, 이거 정말 훌륭하더라. 물론 Virtual 함수를 Hooking하는 것은 이리저리 많은 삽질을 거쳐야 했지만, 프로그래밍이란게 한번 알고 나면 너무 간단해 허무한 것이다. 고로 지금 허무하다.

 

설치.

Detours Hooking Framework 이 사이트에서 Detours Express 2.1을 다운로드 받고 설치하자. default 경로는 C:\Program Files (x86)\Microsoft Research\Detours Express 2.1 이다. 해당 폴더에 Command Prompt로 이동해 nmake all 명령을 실행하자. 본인은 nmake all 명령이 실패했는데 나의 문제는 Visual Studio의 Command Prompt를 이용하고 set DETOURS_TARGET_PROCESSOR =x86 을 설정함으로써 해결할 수 있었다.

 

Function Hooking

이제 Detours를 사용해보자. Visual Studio에 DetoursTest를 만들고 C/C++, Linker 환경을 설정하자. C/C++의 Additional Include Directories에 Detours Express의 Include 디렉토리를 추가하고, Linker의 Additional Library Directories에 Detours Express의 lib 디렉토리를 추가한다. 그리고 Linker의 Additional Dependencies에 detoured.lib, detours.lib를 추가한다. 그후 main.cpp란 파일을 추가 한 후 다음 코드를 넣자.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

void TestFunction(){
    printf("TestFunction Called\n");
}

void HookingFunction(){
    printf("before - TestFunction call");
    TestFunction();
    printf("after - TestFunction call");
}

int main(int, char**){
    TestFunction();
    getchar();
}

main에서 TestFunction을 call했으므로 당연히 CMD 창에 "TestFunction Called" 문장이 나타날 것이다. 이제 TestFunction을 Hooking해 TestFunction()을 호출하면 HookingFunction()이 호출되게 할 것이다. 그리고 이 일은 굉장히 간단하다. 프로그램을 다음과 같이 바꿔보자.


#include <Windows.h>
#include <stdio.h>
#include <detours.h>

void TestFunction(){
    printf("TestFunction Called\n");
}
void (*pfTrueTestFunction)() = TestFunction;
void HookingFunction(){
    printf("before - TestFunction call\n");
    TestFunction();
    //pfTrueTestFunction();
    printf("after - TestFunction call\n");
}

int main(int, char**){
    DWORD error;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach((PVOID*)&pfTrueTestFunction, HookingFunction);
    error = DetourTransactionCommit();

    if (error == NO_ERROR) {
        printf("\n");
    }
    else{
        printf("fail to attach\n");
    }
    TestFunction();
    getchar();
}

프로그램을 실행하면 어떤 일이 벌어질까? before - TestFunction Call 문장이 반복되어 출력될 것이다. 원하는 동작을 원한다면 HookingFunction의 TestFunction()을 주석처리하고 pfTrueTestFunction function pointer로 기존 메소드를 출력하면 된다.

어떻게 이 일이 가능하게 되었을까? DetourAttach 함수 콜에 Breakpoint를 설정하고 TestFunction과 pfTrueTestFunction의 값을 확인해보자.

TestFunction 주소 값과 pfTrueTestFunction 값이 다르지만 pfTrueTestFunction 주소 값의 Assembly 명령어를 확인하면 Jmp 명령어로 TestFunction으로 점프하는 코드가 있음을 확인 할 수 있다. 그리고 그 명령어 위에 'TestFunction:'이 있는데 이것이 함수 symbol값이다. 

이제 DetourAttach명령과 DetourTransactionCommit()까지 프로그램을 진행시켜보자. Watch창으로 TestFunction과 pfTrueTestFunction을 다시 확인해보자. 재미있게도 pfTrueTestFunction의 주소 값이 바뀌었다.

pfTrueTestFunction의 값이 변경되었음을 알 수 있다. 0x010c0060메모리 주소를 보면 TestFunction의 메모리로 점프하는 코드가 있다.

 

더 재미있는 것은 0x010e12bc의 메모리주소의 값이 변경되었다는 사실에 있다.

TestFunction: 심볼 값에 Jmp TestFunction 명령이 있었는데 Jmp HookingFunction으로 변경되었다. 이것이 DetourAttach명령과 DetourTransactionCommit이 하는 일이다. 이것으로 후킹이 완료되었다. 

 

마치며

Detours로 특정 함수 심볼 값을 변경해 후킹을 해보았다. 이런 방식으로 기존의 exe파일이나 dll파일을 후킹할 수도 있다. detours의 bin 폴더 내에는 setdll이란 명령어가 있는데 이를 이용하면 된다. 이 setdll명령은 기존의 exe파일이나 dll파일에 외부의 dll을 로드하게하는 명령어이다. 예를 들어
setdll /d:dllInject.dll notepad.exe
명령을 실행시키면 이후에 notepad.exe를 실행했을 때 프로그램 내부에서 dllInject.dll이 로드되게 된다. 즉 LoadLibrary("dllInject.dll")이 실행되게 되는 것이다. 이 dll이 로드되면 dll내부의 DllMain함수가 실행되게 되고, 만약 DllMain함수 내에서 DetourAttach 등으로 Hooking동작을 하는 코드가 있다면 notepad.exe내에서 호출하는 함수, API들을 Hooking할 수 있다.
 

 2로 계속됩니다.

** 2001. 07. 01 수정.
setdll /d:dllInject.dll executable.exe를 실행시키면 dllInject.dll이 executable.exe에 Inject된다고 위에 설명했었다. 이것은 사실이다. 그런데 난 멋도 모르고 어떻게 추측했었냐면, .text의 EntryPoint에 LoadLibrary("dllInject.dll") 코드를 명시적으로 실행시켜 dllInject.dll의 DllMain 함수가 실행되게 할 줄 알았다. 하지만 사실은 이와 달라 정정한다. Dll Loading의 방식에는 2가지가 있는데 그 중 하나는 명시적 로딩, 즉 코드 내에서 LoadLibrary("dllInject.dll")이라고 명시적으로 코딩을 하는 방법이고, 또 하나는 암시적 로딩으로써 프로그램이 시작하는 순간에 같이 로딩되는 방법이 있다. 암시적 로딩을 위해 PE 파일은 어떤 DLL파일을 로드 해야 하는지 명시해 PE Loader에게 알려야 한다. Detours 는 PE파일의 이 부분을 변경한다. 즉, dllInject.dll이 필요하다고 PE Loader에게 알리는 것이다. 자세한 부분은 www.reversecore.com/23에 잘 설명되어 있다. 

신고

DataGrid and Dynamic Column -- Part2

프로그래밍 2011.04.10 16:51 Posted by 아일레프

지난 Post, DataGrid and Dynamic Column – Part1에서 DataGrid의 Column을 컴파일 타임 이전이 아니라 Runtime에 결정할 수 있는 하나의 예시를 보였다. 이는 Type에 index Property, “[ ]” 가 존재한다는 것과 DataGridTextColumn의 Binding에 사용되는 Binding객체의 Path에 “[ ]”를 사용할 수 있기 때문에 가능한 것이었다. 하지만 이 방법은 논리와 View가 밀접하게 연관되어 MVVM과 같은 Pattern에서 기꺼이 사용하기에는 뭔가 아쉬움이 남게된다. Part2에서는 이를 해결할 수 있는 또 다른 해법을 제시한다.

TypeDescriptor 그리고 ICustomTypeDescriptor

지난 Post에 DataGrid에 사용될 Column은 DataGrid에 바인딩 되는 클래스의 Property에 연관을 가지기에 Property를 어떻게 Dynamic하게 설정할 수 있는지를 고민해야 한다고 말한 바 있는데, 이는 ICustomTypeDescriptor의 존재로 해결 될 수 있다. 

WPF의 ItemsControl와 ItemsControl을 상속받는 ListBox, ComboBox, DataGrid등의 컨트롤들은 ItemsSource에 바인딩된 IEnumerable 객체를 바로 Items에 사용하지 않고 ICollectionView라는 형태로 변환해 사용한다. 이 ICollectionView는(예를 들어 ListCollectionview)는 TypeDescriptor를 적극적으로 사용하는데, TypeDescriptor는 ICustomTypeDescriptor를 구현하는 Type에 대해 별도의 동작을 하게 된다. ICustomTypeDescriptor, Part 1 를 보면 이에 대해서 자세히 확인 할 수 있으며 그 내용의 핵심은 아래 그림으로 간단히 요약될 수 있다. 

즉 TypeDescriptor는 ICustomTypeDescriptor를 구현하는 객체에 대해서는 Reflection이 아니라 ICustomTypeDescriptor의 GetProperties라는 메소드로 Property를 얻어오게 된다는 것이다. 따라서 DataGrid에 바인딩 될 객체가 ICustomTypeDescriptor를 구현하게 하고, DataGrid의 AutoGenerateColumn을 true로 설정함으로써 별도의 Column설정 없이 원하는 화면을 얻을 수 있다. 이때 Column Header는 Property의 이름과 동일하게 된다.

 

ICustomTypeDescriptor를 구현하는 DataForBinding 객체를 제작

public interface ICustomTypeDescriptor
{
    AttributeCollection GetAttributes();
    string GetClassName();
    string GetComponentName();
    TypeConverter GetConverter();
    EventDescriptor GetDefaultEvent();
    PropertyDescriptor GetDefaultProperty();
    object GetEditor(Type editorBaseType);
    EventDescriptorCollection GetEvents();
    EventDescriptorCollection GetEvents(Attribute[] attributes);
    PropertyDescriptorCollection GetProperties();
    PropertyDescriptorCollection GetProperties(Attribute[] attributes);
    object GetPropertyOwner(PropertyDescriptor pd);
}

ICustomTypeDescriptor를 구현하기 위해선 위와 같은 여러 메소드들을 만들어야 한다는 부담감이 드는데, 여기서는 GetClassName, GetComponentName, GetPropertiess, GetPropertyOwner만을 설정해도 충분하다. 그리고 이중 GetProperties메소드가 가장 큰 역할을 하게 된다. 우리가 원하는 화면을 다시 한번 확인해보자.

위 화면을 보면 우리가 1개의 “Key” Property와 N개의 “날짜” Property을 필요로 한다는 것을 알 수 있다. 즉 GetProperties가 반환하는 PropertyDescriptorCollection내에 1개의 “Key”에 해당 되는 PropertyDescriptor와 N개의 “날짜” PropertyDescriptor가 포함되게 되면 된다. 좋다. 구현을 해보자.

public class DataForBinding : ICustomTypeDescriptor
{

    //중략..

    IEnumerable<Data> originalDatas;
    string key;

    PropertyDescriptorCollection propertyDescriptorCollection = new PropertyDescriptorCollection(null);

    public DataForBinding(IEnumerable<Data> datas, IEnumerable<DateTime> datesForDisplay)
    {
        if (datas == null)
            throw new ArgumentNullException("datas");
        if (datas.Count() == 0)
            throw new ArgumentException();
        if (datesForDisplay == null)
        {
            throw new ArgumentException("datesForDisplay");
        }

        this.originalDatas = datas;
        this.key = datas.First().Type;
        if (!datas.All(d => d.Type == key))
        {
            //하나의 Row에 대응되는 Data들은 모두 Type이 동일해야 하므로
            throw new ArgumentException();
        }

        propertyDescriptorCollection.Add(new KeyPropertyDescriptor(key));
        foreach (DateTime dateTime in datesForDisplay)
        {
            propertyDescriptorCollection.Add(new DatePropertyDescriptor(datas, dateTime));
        }
    }

    public PropertyDescriptorCollection GetProperties()
    {
        return propertyDescriptorCollection;
    }

    public string GetClassName()
    {
        return typeof(DataForBinding).Name;
    }

    public string GetComponentName()
    {
        return typeof(DataForBinding).Name;
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }
}

위에서 Bold체로 표시된 부분을 보면, 1개의 KeyPropertyDescriptor와 N개의 DatePropertyDescriptor객체가 PropertyDescroptorCollection에 포함되게 됨을 알 수 있다. 이제 남은 일은 이 각 Property에 해당되는 KeyPropertyDescriptor와 DatePropertyDescriptor를 구현하는 일이다.


KeyPropertyDescroptor와 DatePropertyDescroptor의 구현 

KeyPropertyDescriptor와 DatePropertyDescriptor는 PropertyDescriptor라는 Abstract클래스를 상속받아 구현된다. 이를 위해 다음과 같은 메소드들을 구현해야 하는데 우리가 보여주는 DataGrid는 모두 ReadOnly로 동작하기 때문에 Set, Reset 등의 프로퍼티는 구현할 필요가 없다.

public override bool CanResetValue(object component)
public override Type ComponentType
public override object GetValue(object component)
public override bool IsReadOnly
public override Type PropertyType
public override void ResetValue(object component)
public override void SetValue(object component, object value)
public override bool ShouldSerializeValue(object component)
그럼 KeyPropertyDescriptor부터 구현해보자. 
public class KeyPropertyDescriptor : PropertyDescriptor
{
    string key;
    public KeyPropertyDescriptor(string key):base("Key", null)
    {
        this.key = key;
    }

    #region PropertyDescroptor methods
    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(DataForBinding); }
    }

    public override object GetValue(object component)
    {
        return key;
    }

    public override bool IsReadOnly
    {
        get { return true; }
    }

    public override Type PropertyType
    {
        get { return typeof(string); }
    }

    public override void ResetValue(object component)
    {
        throw new InvalidOperationException("이 프로퍼티는 ReadOnly로만 동작해야 한다.");
    }

    public override void SetValue(object component, object value)
    {
        throw new InvalidOperationException("이 프로퍼티는 ReadOnly로만 동작해야 한다.");
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
    #endregion
}

Key 프로퍼티의 구현은 위와 같이 간단하다. 이는 DatePropertyDescriptor도 마찬가지이다. 단, 차이가 있다면 Property이름의 제약조건으로 인해 “2009/10/2”라는 Property를 그대로 사용할 수 없다. 따라서 약간의 변환이 필요하다.

public class DatePropertyDescriptor : PropertyDescriptor
{
    private IEnumerable<Data> datas;
    private DateTime dateTime;

    public DatePropertyDescriptor(IEnumerable<Data> datas, DateTime dateTime) : 
        base(string.Format("P{0}_{1}_{2}",dateTime.Year, dateTime.Month, dateTime.Day), null)
    {
        // TODO: Complete member initialization
        this.datas = datas;
        this.dateTime = dateTime;
    }

    #region PropertyDescroptor methods
    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(DataForBinding); }
    }

    public override object GetValue(object component)
    {
        StringBuilder stringBuilder = new StringBuilder();
        foreach (Data data in datas.Where(d => d.DateTime.Day == dateTime.Day && d.DateTime.Month == dateTime.Month))
        {
            stringBuilder.AppendLine(string.Format("{0:00}:{1:00}", (int)data.DateTime.TimeOfDay.TotalHours, data.DateTime.TimeOfDay.Minutes)
                                        + " - " + data.Detail);
        }
        return stringBuilder.ToString();
    }
    //후략…
    #endregion
}



DataGrid ItemssSource설정, Header설정

이제 모든 준비가 끝났다. 남은 것은 서비스로 부터 얻은 데이타를 DataForBinding객체로 변환해 DataGrid의 ItemsSource로 설정하는 일이다.

public MainWindow()
{
    InitializeComponent();
    IEnumerable<Data> datas = GetData(); //Data Service로 부터 원본 Data집합을 얻어낸다.
    this.dataGrid.ItemsSource = new ArrayList(GetBindingData(datas).ToList());//바인딩을 위한 객체로 변환한다.       
}

private IEnumerable<DataForBinding> GetBindingData(IEnumerable<Data> datas)
{
    IEnumerable<IGrouping<string, Data>> groupDatas = datas.GroupBy(data => data.Type);

    foreach (IGrouping<string, Data> group in groupDatas)
    {
        yield return new DataForBinding(group, datas.GroupBy(d => d.DateTime.Date).OrderBy(s => s.Key).Select(s => s.Key));
    }
}
<DataGrid x:Name="dataGrid"  AutoGenerateColumns="True" IsReadOnly="true" />

주목해야 할 것은 별도의 Column설정이 필요하지 않으므로 이 부분이 View와 독립적으로 이루어질 수 있다는 점이다. 따라서 이를 MVVM패턴으로 변환하는 것도 간단하다.  또한 유의 해야 할 점은 new ArrayList(GetBindingData(datas).ToList());  와 같이 ArrayList로 변환해 ItemsSource를 설정해야 한다는 점이다. List나 IEnumerable객체를 바로 사용하면 DataGrid가 ICustomTypeDescriptor의 GetProperties를 이용하지 않는다.(이 부분은 미지로 남아있다.) 여하튼, 이 코드를 실행시키면 다음과 같은 화면을 얻을 수 있다.

하지만 보시다시피 Column의 Header가 P2009_10_1과 같이 표현되므로 이를 2009/10/1 로 표시하는 작업을 따로 해주어야 한다.

    <DataGrid x:Name="dataGrid"  AutoGenerateColumns="True" IsReadOnly="true" >
        <DataGrid.Resources>
            <local:GridColumnConverter x:Key="GridColumnConverter"/>
            <Style TargetType="DataGridColumnHeader">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <TextBlock Text="{Binding Converter={StaticResource GridColumnConverter}}"/>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </DataGrid.Resources>
    </DataGrid>

위 설정을 마치면 다음과 같이 우리가 워하는 화면을 볼 수 있다.

 

이 방법의 문제점

ICustomTypeDescriptor를 사용하는 이 방법은 Part1의 방법과 같은 DataGridColumn의 작업을 별도로 요구하지 않기에 View와 논리를 분리할 수 있다는 장점이 있다. 그러나 아쉬운 것은 이를 위해 ICustomTypeDescriptor와 PropertyDescriptor를 구현하는 별도의 객체를 매번 만들어 줘야 한다는 것이다. 이를 해결할 수 있을까? .NET 4에는 ExpandoObject라는 객체가 추가됨으로 이 문제에 대한 해결의 가능성을 보여준다. 그러나 ExpandoObject만을 사용하는 것으로는 문제가 해결되지 않음을 확인했다. 몇가지 작업이 필요한 것 같은데 이 작업이 성공한다면 Part3 포스트가 나올 수 있을 것이다.

신고

DataGrid and Dynamic Column -- Part1

프로그래밍 2011.04.01 15:08 Posted by 아일레프
public class Data
{
    public string Type { get; set; }
    public DateTime DateTime { get; set; }
    public string Detail { get; set; }
}

위 Type 으로 이루어진 데이터의 집합이 다음과 같을 때,

<local:Data Type="A" DateTime="10-01-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-01-2009 21:56:01" Detail="활동 2"/>
<local:Data Type="A" DateTime="10-01-2009 23:56:01" Detail="활동 3"/>
<local:Data Type="A" DateTime="10-02-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-02-2009 21:56:01" Detail="활동 1"/>
<local:Data Type="B" DateTime="10-02-2009 14:56:01" Detail="활동 4"/>
<local:Data Type="C" DateTime="10-03-2009 18:56:01" Detail="활동 2"/>

이 7개의 Data를 아래와 같이 표시해야 한다고 해보자. "Type"이라는 Key로 grouping한 데이터를 날짜 순으로 표시하는 것이 목적이다.

이 경우 역시 DataGrid를 떠올리게된다. 그런데 DataGrid를 이용하기 위해 가장 먼저 생각해야 하는 것은 Column의 설정이다. 보통 DataGrid.Columns에 이를 설정해야 하는데 이 경우에는 쉽지 않다. Column으로 사용되게 되는 날짜 정보를 컴파일 이전 타임에 결정할 수 없기 때문이다. 당장 위 경우, {D, 2009/10/04 07:21, 활동 1} 데이터가 추가되게 되면 "2009 /10/4"라는 Column이 필요하게 된다. 따라서 우리는 Runtime에 어떻게 DataGrid의 Column을 설정할 수 있는가? 라는 질문을 던져야 하고 이에 대한 해답을 얻어야 한다. 또한 Column을 Runtime에 결정하는 것 외에 또 한가지 문제가 남는다. DataGrid의 Column은 ItemSource에 바인딩된 개개의 Item의 Property에 대응되게 되므로 바인딩된 Item객체의 Property를 어떻게 Dynamic하게 만들수 있는가? 라는 질문에 답해야 하는 것이다.

해답은 다양하게 있을 수 있다. 나는 3개의 가능성을 떠올렸고 이 가운데 2개는 동작하는 것을 확인했다.(나머지 하나는 확실하지 않다.) 이번 포스트는 첫번째 해답에 대해 다룬다.

 

DataGrid에 바인딩될 객체 만들기

이를 해결하기 위해 DataBase에서 얻어온 객체를 그대로 사용하는것이 아니라 DataGrid의 Row를 표현할 수 있는 새로운 class를 정의하기로 한다. 그런데 어떻게 Property를 Dynamic하게 만들 수 있을까? 사실 모든 Type의 Property는 컴파일 타임 이전에 결정된다. 그런데 단 하나의 Property는 Runtime에 결정될 수 있다. 바로 index Property "[ ]"가 바로 그것이다. 이를 이용하자.

DataGrid에 표현될 Column들은 "Key"와 "날짜" 요소들이므로 바인딩을 위한 클래스는 다음과 같이 설계될 수 있을 것이다.

public class DataForBinding : INotifyPropertyChanged
{
    private IEnumerable<Data> originalDatas;
    public string Key { get; private set; }
    public string this[string day] //yyyy/MM/dd Format의 String이 Input으로 들어오게 된다.
    {
        get 
        {
            DateTime dateTime;
            if(!DateTime.TryParseExact(day, "yyyy/MM/dd",null, System.Globalization.DateTimeStyles.None, out dateTime))
            {
                throw new ArgumentException();
            }
            StringBuilder stringBuilder = new StringBuilder();
            foreach (Data data in originalDatas.Where(d => d.DateTime.Day == dateTime.Day && d.DateTime.Month == dateTime.Month))
            {
                stringBuilder.AppendLine(string.Format ("{0:00}:{1:00}", (int)data.DateTime.TimeOfDay.TotalHours,data.DateTime.TimeOfDay.Minutes) 
                                            + " - " + data.Detail);
            }
            //19 : 56 – 활동 1
            //21 : 56 – 활동 2
            //23: 56 – 활동 3 과 같은 string을 반환한다.
            return stringBuilder.ToString();
        } 
    }

    public DataForBinding(IEnumerable<Data> datas)
    {
        if (datas == null)
            throw new ArgumentNullException("datas");
        if (datas.Count() == 0)
            throw new ArgumentException();

        this.originalDatas = datas;
        this.Key = datas.First().Type;
        if (!datas.All(d => d.Type == Key))
        {
            //하나의 Row에 대응되는 Data들은 모두 Type이 동일해야 하므로
            throw new ArgumentException();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

이제 Service로 부터 얻은 Data집합을 DataForBinding 집합으로 변환해야 할 것이다.

IEnumerable<Data> datas = GetData(); //Data Service로 부터 원본 객체를 얻는다.
IEnumerable<DataForBinding> datasForBinding = GetBindingData(datas); //바인딩을 위한 객체로 변환한다.

이 경우 GetBindingData는 다음과 같이 구현될 수 있다.

private IEnumerable<DataForBinding> GetBindingData(IEnumerable<Data> datas)
{
    IEnumerable<IGrouping<string, Data>> groupDatas = datas.GroupBy(data => data.Type);

    foreach (IGrouping<string, Data> group in groupDatas)
    {
        yield return new DataForBinding(group);
    }
}   

DataGrid의 Column 설정하기

바인딩될 Data는 위와 같이 손쉽게 만들 수 있었다. 이제 DataGrid의 Column을 설정하는 일이 남았다. 이 경우 DataGrid의 Column객체를 다음과 같이 설정하면 된다. 이는 Binding객체가 index 프로퍼티에도 적용될 수 있기에 가능한 것이다.

private void SetDataGridColumn(IEnumerable<DataForBinding> datasForBinding, IEnumerable<Data> datas)
{
    dataGrid.ItemsSource = datasForBinding;

    //Add Key Columns
    DataGridTextColumn keyTextColumn = new DataGridTextColumn();
    keyTextColumn.Header = "Key";
    Binding binding = new Binding("Key");
    keyTextColumn.Binding = binding;
    dataGrid.Columns.Add(keyTextColumn);


    //Add Date Columns
    IEnumerable<string> columnStrings = datas.GroupBy(data => data.DateTime.Date).Select(group => group.Key.ToString("yyyy/MM/dd"));

    foreach (string columnString in columnStrings)
    {
        DataGridTextColumn dateTextColumn = new DataGridTextColumn();
        dateTextColumn.Header = columnString;
        Binding bindingForDate = new Binding("[" + columnString + "]");
        dateTextColumn.Binding = bindingForDate;
        dataGrid.Columns.Add(dateTextColumn);
    }
}

 



이 방법의 문제점

그런데 이 방법은 아쉬움이 깊게 남는다. 왜인가? View와 분리되어야 할 논리 클래스에서 DataGrid, DataGridTextColumn과 같은 UI 객체를 그대로 사용하고 있기 때문이다. 이 경우 View와 논리 클래스 사이의 의존도로 인해 View가 변경되었을 때 어쩔 수 없이 논리 클래스도 변경되어야 하는 안타까운 일이 생길 수 있다. 만약 여러분의 논리 클래스에서 UIElement객체를 직접 참조하거나 new 키워드를 통해 생성하는 코드가 있다면 부지런히 다른 방법을 탐구해보아야 한다. Part2에서는 이를 해결한 해답을 보여주기로 한다.

 

신고

GenericTypeExtension works in .NET4.0

프로그래밍 2011.03.23 18:41 Posted by 아일레프

-- I'm sorry for my poor English. --

There are many "GenericTypeExtension" implementations.


http://stackoverflow.com/questions/1706123/how-to-reference-a-generic-type-in-the-datatype-attribute-of-a-hierarchicaldatate

http://blogs.msdn.com/b/mikehillberg/archive/2006/10/06/limitedgenericssupportinxaml.aspx

http://windowsclient.net/blogs/rob_relyea/archive/2009/06/01/xaml-using-generic-types-in-xaml-2009.aspx


All of these don't work in .NET4. Why? These GenericTypeExtensions use IXamlTypeResolver and IXamlTypeResolver.Resolve("generic:List`1") correctly returns "typeof(List<>)" in .NET3.0. However, it can't resolve in .NET 4.0. "IXamlTypeResolver.Resolve("generic:List`1")" just throw an Exception in .NET4.

"Character '`' was unexpected in string 'generic:List`1'.  Invalid XAML type name."

But don't be afraid. There are other useful services NET4.0 provide. If you used IXamlNamespaceResolver and IXamlSchemeaContextProvider, you can get a generic type easily.


xmlns:generic="clr-namespace:System.Collections.Generic;assembly=mscorlib"
XamlNamespaceResolver nameResolver = 
                                serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlNamespaceResolver;
IXamlSchemaContextProvider schemeContextProvider = 
                                serviceProvider.GetService(typeof(IXamlSchemaContextProvider)) as IXamlSchemaContextProvider;
XamlTypeName xamlTypeName = new XamlTypeName(nameResolver.GetNamespace("generic"), "List`1");
Type genericType = schemeContextProvider.SchemaContext.GetXamlType(xamlTypeName).UnderlyingType;

If you explore the ServiceProviderContext through Reflector, you can find other services the ServiceProviderContext implement.

ITypeDescriptorContext, IXamlTypeResolver, IUriContext, IAmbientProvider, 
IXamlSchemaContextProvider, IRootObjectProvider, IXamlNamespaceResolver, 
IProvideValueTarget, IXamlNameResolver, IDestinationTypeProvider

 

I attached a complete GenericTypeExtension, please enjoy it. Thanks~!


[ContentProperty("TypeArguments")]
public class GenericTypeExtension : MarkupExtension
{
        
    private Collection<Type> _typeArguments = new Collection<Type>();
    public Collection<Type> TypeArguments
    {
        get { return _typeArguments; }
    }

    // generic:List`1
    private string baseTypeName;
    public string BaseTypeName
    {
        get { return baseTypeName; }
        set { baseTypeName = value; }
    }
    public GenericTypeExtension() { }
    public GenericTypeExtension(string baseTypeName) { this.baseTypeName = baseTypeName; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (string.IsNullOrEmpty(baseTypeName))
            throw new ArgumentNullException("BaseTypeName");
        string[] baseTypeArray = baseTypeName.Split(':');

        if (baseTypeArray.Length != 2)
            throw new ArgumentException("BaseTypeName");

        if (TypeArguments.Count == 0)
            throw new ArgumentException("TypeArguments");

        IXamlNamespaceResolver nameResolver = 
                                        serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlNamespaceResolver;
        IXamlSchemaContextProvider schemeContextProvider = 
                                        serviceProvider.GetService(typeof(IXamlSchemaContextProvider)) as IXamlSchemaContextProvider;

        if (nameResolver == null || schemeContextProvider == null)
        {
            IRootObjectProvider rootObjectProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
            if (rootObjectProvider as DependencyObject != null &&  
               !DesignerProperties.GetIsInDesignMode(rootObjectProvider as DependencyObject))
                throw new Exception("This Generic markup extension requires these services");
            else
                return null;
        }

        XamlTypeName xamlTypeName = new XamlTypeName(nameResolver.GetNamespace(baseTypeArray[0]), baseTypeArray[1]);
        Type genericType  = schemeContextProvider.SchemaContext.GetXamlType(xamlTypeName).UnderlyingType;
        Type[] typeArguments = TypeArguments.ToArray();

        return genericType.MakeGenericType(typeArguments);
    }
}


        <local:GenericTypeExtension BaseTypeName="generic:List`1">
            <x:Type TypeName="system:String"/>
        </local:GenericTypeExtension>

신고

가상화 지원 Panel을 구현하기 위해 가장 어려운 부분은 보여지지 않는 영역의 크기를 추정하는 것이라고 말한 바 있다.이 "추정"에 따른 한계에 의해 WPF에 포함된 VirtualizingStackPanel은 바인딩된 개별 Item의 크기가 각각 다르고 총 Item의 수가 적을 때 스크롤바의 Thumb크기가 일정하지 않은 문제가 발생한다.

이를 확인하기 위해 아래의 코드로 실험해 보았다.

public class DataItem
{
    public string Content { get; set; }
    public double Height { get; set; }
}
 
public partial class SeeMoreStackPanelTest : UserControl
{
    public SeeMoreStackPanelTest()
    {
        InitializeComponent();
 
        this.DataContext = GetDataItems(50);
    }
    private IEnumerable<DataItem> GetDataItems(int count)
    {
        Random random = new Random((int)DateTime.Now.Ticks);
        //높이가 20~ 250인 DataItem을 반환한다.
        for (int i = 0; i < count; i++)
        {
            yield return new DataItem { Content = i.ToString(), Height = random.Next(230) + 20};
        }
    }
}

<ListBox ItemsSource="{Binding}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border Width="100"
                Height="{Binding Height}" BorderThickness="1" BorderBrush="Black" CornerRadius="5"> 
                <StackPanel>
                    <TextBlock Text="{Binding Content}"/>
                    <TextBlock Text="{Binding Height}"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>

위 코드로 동작되는 ListBox는 화면에 보이는 ListBoxItem의 크기에 따라 우측 ScrollViewer의 Thumb 크기가 들쑥날쑥 변하게 된다.

또 위 프로그램을 실행해보면 한가지 문제가 더 있음을 발견하게 되는데 그것은 "Smooth Scroll"을 사용할 수 없다는 것이다. ScrollViewer는 항상 ItemContainer의 크기 단위로 Offset이 변하게 되는데 이것을 원하지 않아 ScrollViewer.CanContentScroll을 false로 준다면 당장 VirualizingStackPanel의 가상화 동작을 지원받을 수 없게 된다.

  1. ItemContainer의 크기가 가변적일 때 ScrollViewer의 Thumb가 일정하지 않다.
  2. 가상화 기능과 Smooth Scroll을 동시에 사용할 수 없다.

이 두가지 문제를 해결해 보자. 1번 문제는 ScrollViewer의 Extent 크기를 "추정"에서 얻어오기에 발생하는 문제이므로 기존의 시나리오로는 완벽히 해결할 수 없다. 이럴 땐 돌아가야 한다. Google Reader와 같은 Scroll방식을 사용하면 어떨까?

*GoogleReader의 ScrollViewer의 모습. 한번에 모든 Data를 Query하는 것이 아니라 Scroll Thumb가 가장 하단에 위치했을 때 일부 Data를 추가로 조회해 오고 있다. 살펴보니 GoogleReader는 UI Virtualization과 Data Virtualization을 매우 적절히 활용하고 있음을 확인할 수 있었다. *

GoogleReader와 같은 Scroll방식을 적용하면 얻어진 ItemContainer에 대해선 정확한 사이즈를 얻어낼 수 있으므로 1번의 문제가 해소된다. 2번의 문제는 가상화 Panel을 직접 구현하면 매우 쉽게 해결 할 수 있다. 좋다. 시작해보자.

1단계. 이름 정하기, IScrollInfo 인터페이스 구현하기

고민끝에 이름을 SeeMoreStackPanel로 하기로 했다. Naming은 어렵다. ㅜ.ㅜ 그리고 IScrollInfo 인터페이스를 구현해야 한다.

public double VerticalOffset
{
    get;
    private set;
}
 
public void SetHorizontalOffset(double offset)
{
    if (offset < 0 || viewportSize.Height >= ExtentHeight)
    {
        offset = 0;
    }
    else
    {
        if (offset + viewportSize.Height >= ExtentHeight)
        {
            offset = ExtentHeight - viewportSize.Height;
        }
    }
    VerticalOffset = offset;
 
    ScrollOwner.InvalidateScrollInfo();
    InvalidateMeasure();
}
 
public void SetVerticalOffset(double offset)
{
    throw new NotImplementedException();
}
 
public void LineDown()
{
    SetVerticalOffset(VerticalOffset + 5);
}
 
public void LineUp()
{
    SetVerticalOffset(VerticalOffset - 5);
}
//이것은 IScrollInfo Interface의 일부 구현이다.

LineUp, LineDown이 불렸을 때 VerticalOffset의 차이를 5로 두었다. 이것으로 Smooth Scrolling이 가능해진다. 그 어떤 값을 두어도 상관 없을 것이다. 나의 편의를 위해 Left, Right등의 Horizontal 동작의 기능은 구현하지 않았다.

2단계. ItemContainer들을 생성해 표시해보기

일단 VerticalOffset이 0인 경우 만을 생각하기로 하자. 2단계를 위해 ItemContainer를 만들고, ScrollViewer정보를 업데이트하고 ItemContainer들을 배치해야 한다. 이를 위해 MeasureOverride, ArrangeOverride 메소드를 override해 구현한다.

MeasureOverride메소드를 보자. 사실 가상화 지원 패널의 반은 이 메소드의 구현에 있다고 해도 과언이 아니다. 이 메소드 내에서 우리는 ItemContainer를 생성하고, ScrollViewer의 정보를 업데이트 하고, 더 이상 필요하지 않은 ItemContainer를 삭제해야 한다. 이 중 ItemContainer를 삭제하는 것은 선택사항이므로 당연히(?) 여기선 구현하지 않는다. 아래 코드에서 Assertion을 주목해 읽어주면 되겠다.

protected override Size MeasureOverride(Size availableSize)
{
    if (VerticalOffset != 0)
        throw new InvalidOperationException("2단계에서는 VerticalOffset이 0인 경우만 고려한다.");
 
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl == null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double remainHeight = availableSize.Height;
    UIElementCollection children = this.InternalChildren;
    GeneratorPosition position = new GeneratorPosition(-1,0);
    IItemContainerGenerator generator = ItemContainerGenerator as IItemContainerGenerator;
    using (generator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (remainHeight > 0)
        {
            bool isRealized;
            UIElement element = generator.GenerateNext(out isRealized) as UIElement;
            if (element == null) break;
            if (isRealized) //새로 생성된 ItemContainer
            {
                Debug.Assert(InternalChildren.Contains(element) == false,
                                         "새로 얻은 element이므로 InternalChildren은 이 element를 포함하지 않는다.");
                this.AddInternalChild(element);
                //이 PrepareItemContainer메소드가 어떤 역할을 하는지 알고 싶다면 이 PrepareItemContainer메소드 호출을 제거하고 
                //프로그램을 실행시키면 된다. 
                //PrepareItemContainer메소드 호출을 제거하면 Measure 호출 후
                // element.DesiredSize 값이 정상적으로 얻어지지 않는다. 
                //추측하건데 PrepareItemContainer에 ItemTemplate으로 VisualTree를 얻어 
                //ListBoxItem의 Child로 설정하는 부분이 있지 않을까 싶다.
                ItemContainerGenerator.PrepareItemContainer(element);
                element.Measure(availableSize);
            }
            else
            {
                Debug.Assert(InternalChildren.Contains(element));
            }
            remainHeight -= element.DesiredSize.Height;
        }
    }
    this.extentSize = availableSize;
    if (remainHeight < 0)
    {
        this.extentSize.Height += remainHeight * -1;
    }
    this.viewportSize = availableSize;
    this.ScrollOwner.InvalidateScrollInfo();
    return availableSize;
}

VerticalOffset이 0인 경우, 즉 ItemsControl이 처음 로드 되었을 때의 경우만 고려했으므로 코딩이 이정도로 끝나게 되었다. 이것으로 ItemsControl이 로드되었을 때 화면내에 보여질 만큼의 ListBoxItem만 생성되게 된다. 생성된 ItemContainer를 배치하는 ArrangeOverride메소드를 확인해보자.

protected override Size ArrangeOverride(Size finalSize)
{
    if (VerticalOffset != 0)
        throw new InvalidOperationException("2단계에서는 VerticalOffset이 0인 경우만 고려한다.");
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl== null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    IItemContainerGenerator generator = itemsControl.ItemContainerGenerator as IItemContainerGenerator;
    double topPosition = 0;
    GeneratorPosition position = new GeneratorPosition(-1, 0);
    using (generator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (topPosition < finalSize.Height)
        {
            bool isRealized;
            UIElement element = generator.GenerateNext(out isRealized) as UIElement;
            if (element == null) return finalSize;
            Debug.Assert(isRealized == false,
                            "MeasureOverride에 의해 이미 생성된 ItemContainer이어야 하므로 이 값이 false이다.");
 
            Debug.Assert(InternalChildren.Contains(element),
                            "InternalChildren은 이 element를 포함하고 있어야 한다.");
            element.Arrange(new Rect(new Point(0, topPosition), element.DesiredSize));
            topPosition += element.RenderSize.Height; 
        }
    }
    return finalSize;
}

좋다. 이제 3단계에서는 VerticalOffset이 변경되었을 때의 기능을 추가할 것이고, 4단계에서는 Scroll Thumb가 ScrollViewer의 마지막에 닿았을 때 추가적으로 ItemContainer를 생성해 표시하는 기능을 추가할 것이다. 추측하건데, 이 정도로도 머리속에 3,4 단계에 대한 그림이 머리 속에 훤하게 그려진 분이 있을 것이다.

3단계. VerticalOffset이 0보다 큰 값일 경우의 동작 구현

이제 VerticalOffset이 0보다 큰 값일 경우의 동작을 구현해 보자. 먼저 MeasureOverride 메소드를 봐야한다. 다시한번 말하지만 이 메소드는 ItemContainer를 생성, 파괴, ScrollViewer 정보를 업데이트하는 역할을 한다. 그런데 우리의 시나리오에 의하면 이 MeasureOverride가 ItemsContainer를 생성하는 경우는 단 3가지 이다. 첫번째 경우는 ItemsControl이 처음으로 로드되었을 경우이며, 두번째는 ScrollEnd 동작이 수행되었을 때, 즉 VerticalOffset + ViwportHeight = ExtentHeight일 경우이다. 그리고 마지막 경우는 바인딩 된 ItemsSource 자체에 변화가 일어났을 경우이다. 여기서 첫번째, 두번째 경우만 고려해보면 MeasureOverride 메소드는 다음과 같이 될 것이다.

protected override Size MeasureOverride(Size availableSize)
{
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl == null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double remainHeight = availableSize.Height;
    UIElementCollection children = this.InternalChildren;
 
    //새 ItemContainer를 생성할 경우는 다음 두가지 경우 이다. 
    //첫번째는 처음 로딩되었을 경우이며
    //두번째는 ScrollToEnd가 동작되었을 경우이다.
    if ((VerticalOffset == 0 && InternalChildren.Count == 0) || VerticalOffset + ViewportHeight == ExtentHeight)
    {   
        CreateItemContainer(ref availableSize, ref remainHeight);
        this.extentSize = availableSize;
        if (remainHeight < 0)
        {
            this.extentSize.Height += remainHeight * -1;
        }
    }
 
    this.viewportSize = availableSize;
    this.ScrollOwner.InvalidateScrollInfo();
    return availableSize;
}

하나의 If문이 추가되었고 ItemContainer를 생성하는 부분은 CreateItemContainer라는 메소드로 리플랙션을 수행했다. 3단계에서 MeasureOverride메소드의 변경은 이것으로 충분하다. 그럼 이제 생성한 ItemContainer들을 배치할 차례이다. ArrangeOverride메소드는 InternalChildren에 추가된 ItemContainer 들을 VerticalOffset을 고려해 배치하면 된다. 이는 정말 너무 간단하다.

TranslateTransform translateTransform = new TranslateTransform();
public SeeMoreStackPanel()
{
   this.RenderTransform = translateTransform;
}
protected override Size ArrangeOverride(Size finalSize)
{
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl== null)
    {
        throw new InvalidOperationException("단독으로 사용될 수 없는 Panel입니다.");
    }
    double topPosition = 0;
    //InnerContainer에 추가된 Item들을 배치한다. 
    foreach(UIElement element in this.InternalChildren)
    {
        element.Arrange(new Rect(new Point(0, topPosition), element.DesiredSize));
        topPosition += element.RenderSize.Height; 
    }
    translateTransform.Y = VerticalOffset * -1;
    return finalSize;
}

정말 쉽지 않은가? 이것으로 3단계가 끝났다. 이제 남은 것은 ScrollBar가 마지막까지 내려갔을 때 Item을 추가시키기만 하면 된다.

4단계. ScrollToEnd가 동작했을 때, VerticalOffset + ViewportHeight = ExtentHeight 일 경우에 Item을 추가

이것 역시 굉장히 쉽다. CreateItemContainer에 다음을 추가하면 되겠다.

if (VerticalOffset == 0 && InternalChildren.Count == 0)
 
   //처음 ItemsControl이 로드되었을 경우의 동작 코드
   // …
   // ...
 
else //ScrollToEnd 명령이 불렸을 경우이다. 이 때는 5개의 Item을 추가적으로 생성하기로 한다.
 
   Debug.Assert(VerticalOffset + viewportSize.Height == ExtentHeight);
 
   //모든 Item들이 추가되었을 경우
   if (InternalChildren.Count == ItemsControl.GetItemsOwner(this).Items.Count) 
       return;
   
   IItemContainerGenerator generator = ItemContainerGenerator as IItemContainerGenerator;
   GeneratorPosition position = generator.GeneratorPositionFromIndex(InternalChildren.Count);
#if DEBUG
   int count = InternalChildren.Count;
#endif
   using (generator.StartAt(position, GeneratorDirection.Forward, true))
   {
       for (int i = 0; i < 5; i++)
       {
           bool isRealized;
           UIElement element = generator.GenerateNext(out isRealized) as UIElement;
           //더 이상 생성할 Item이 없을 경우
           if (element == null) break;
           if (isRealized) //새로 생성된 ItemContainer
           {
               Debug.Assert(InternalChildren.Contains(element) == false, "새로 얻은 element이므로 InternalChildren은 이 element를 포함하지 않는다.");
               this.AddInternalChild(element);
               ItemContainerGenerator.PrepareItemContainer(element);
               element.Measure(availableSize);
               //ScrollViewer의 ExtentSize를 조절한다.
               this.extentSize.Height += element.DesiredSize.Height;
           }
       }
   }
 
#if DEBUG
   Debug.Assert(ItemsControl.GetItemsOwner(this).Items.Count == InternalChildren.Count ||
               count + 5 == InternalChildren.Count);
#endif
 

마치며

어떤가? 금새 가상화 패널을 하나 만들었다. 물론 이것이 완전한 패널의 기능을 하려면 또 다른 여러 경우를 생각해야 한다. 바인딩 된 ItemsSource가 변경되었을 경우에 대한 코드를 추가해야 하며, HorizontalScrollBar의 기능 또한 추가해야 한다. 그리고 추가된 ItemContainer들의 크기가 변경되었을 때의 경우도 고려해보아야한다. 매우 심심하면 한번 직접 구현해 보시라. :)

 

첨부된 파일은 위 SeeMoreStackPanel을 테스트 하기 위한 프로젝트이다. txt파일로 각 단계의 소스코드를 첨부해놓았다. 


신고

WPF UI Virtualization -1

프로그래밍 2011.03.01 14:55 Posted by 아일레프

Virtualization은 Data Virtualization과 UI Virtualization으로 나뉠 수 있다. Data Virtualization이란 필요한 Data만을 데이터 저장소에서 가져와 성능을 향상시키는 방법을 의미하고 UI Virtualization이란 화면에 보여지지 않는 컨트롤 객체를 생성 않는 것으로 속도를 향상시키는 기법을 의미한다. 이 포스트는 UI Virtualization만을 다룬다. 만약 Data Virtualization에 대해 자세히 알고 싶은 분은 놀랍도록 멋진 Data Virtualization in WPF and beyond을 참고하면 되겠다.

UI Virtualization을 좀 더 쉽게 살펴보기 위해 ListBox를 예로 들어보자. 1000개의 int 데이터를 가지는 List<int> 가 ListBox의 ItemsSource에 바인딩 되었다고 가정하면 ListBox의 VisualTree는 다음과 같이 구성될 것이다.

ListBox        
  ScrollViewer      
    ItemsPresenter    
      Panel  
        ListBoxItem
        ListBoxItem
        ListBoxItem
...
...
...

UI 가상화를 사용하지 않는다면 데이터가 1000개 이므로 ListBox가 로드되었을 때 생성될 ListBoxItem들의 숫자는 총 1000개이다. WPF의 UI Virtualization은 "화면에 보여지지 않는 객체를 생성하지 않는 것으로 속도를 향상시키는 기법" 이라고 말했다. 이를 ListBox의 예로 국한하면 화면에 보여지지 않는 불필요한 ListBoxItem을 생성하지 않는 것으로 구체화할 수 있다. ListBox의 예에서 UI Virtualization을 가능하게 하려면 이 ListBoxItem의 생성을 컨트롤 할 수 있어야 한다는 것이다. 그렇다면, ListBoxItem을 생성하게 하는 객체가 무엇인가? 라는 질문을 던져야 한다. 당신은 후보가 다음과 같을 때 어떤 객체가 ListBoxItem을 생성하게 한다고 생각하는가?

  1. ListBox
  2. ItemsPresenter
  3. Panel

 

...

...

...

 

자, 답을 보자. ListBoxItem객체는 ListBox의 GetContainerForItemOverride 메소드를 통해 생성된다. 하지만 위 질문의 답은 ListBox가 아니다. ListBox의 GetContainerForItemOverride메소드는 ListBoxItem을 생성하기 위해 이용당하는 것이다. 잘 살펴보면 GetContainerForItemOverride 메소드를 호출하는 것은 ItemsPresenter내의 ItemContainerGenerator라는 객체이며 이 객체를 이용해 최초에 ListBoxItem을 생성하라는 의지를 발하는 녀석은 "Panel"이다. 만약 StackPanel이 ItemsPanel로 사용되었다면 ItemsSource가 바인딩 된 시점에 다음 코드가 불리게 된다.

public abstract class Panel : FrameworkElement, IAddChild
    internal virtual void GenerateChildren()
    {
        IItemContainerGenerator generator = (IItemContainerGenerator)_itemContainerGenerator; 
        if (generator != null)
        {
            using (generator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward))
            { 
                UIElement child;
                while ((child = generator.GenerateNext() as UIElement) != null) 
                { 
                    _uiElementCollection.AddInternal(child);
                    generator.PrepareItemContainer(child); 
                }
            }
        }
} 

StackPanel은 ListBox의 Items가 변경되었을 때 자신의 Panel의 GenerateChildren()메소드를 그대로 사용해 모든 데이터에 대응되는 ItemContainer를 만들어 자신(StackPanel)에 삽입하는 것을 볼 수 있다. 하여간 중요한 것은 ItemsContainer를 생성하게 만드는 객체는 바로 Panel이라는 사실이다. 따라서 UI Virtualization을 구현하려면 바로 이 Panel을 만지작 거려야 한다.

그런데 Panel만을 수정하는 것으로 UI Virtualization을 구현할 수 있을까? 다른 정보가 필요하지 않을까? 가상화 지원 Panel은 일반적인 Panel(Grid, StackPanel, WrapPanel)과 다르게 특정 시점에 화면에 보여야만 하는 컨트롤만을 생성해 자신의 Children에 추가해야한다. 그런데 이것은 Panel 홀로 할 수 없는 일이다. 어떤 것이 추가로 필요할까? 그렇다. 바로 "화면"에 대한 정보, 곧 ScrollViewer정보를 알 수 있어야 하며 나아가 ScrollViewer를 컨트롤 할 수 있어야 한다. UI Virtualization기능은 가상화 지원 Panel과 ScrollViewer를 동시에 필요로 한다. 둘 중 하나라도 만족하지 못하면 UI Virtualization 기능을 구현할 수 없다. UI Virtualization 기능을 위해선 이 ScrollViewer의 역할이 굉장히 중요하다.

이 ScrollViewer를 살펴보자. WPF ScrollViewer Overview 페이지를 보면 Physical Scroll 과 Logical Scroll의 개념이 나온다. Physical Scroll을 사용하면 Pixel, DIU단위의 스크롤링이 가능하다. 반면 Logical Scroll을 사용하면 ScrollViewer의 스크롤 동작을 수행했을 때 pixel단위로 움직이는 것이 아니라 임의의 논리 단위 만큼의 Height 또는 Width만큼 Scroll의 Offset이 이동하게 된다.(ListBox의 동작을 생각해보라!)

일단 Physical Scroll이 어떻게 가능할 지 생각해보자. ScrollViewer는 자신의 가용 크기를 알아야 하며 자식이 요구하는 크기를 알아야 한다. 이는 모두 Measure동작에서 이루어진다. ScrollViewer는 자식에게 Measure(double.PositiveInfinity, double.PositiveInfinity)를 사용해 자식이 요구하는 최대 크기를 알아낼 수 있고 MesureOverride 메소드에서 들어오는 constraint인수로 자신의 가용 크기를 알아 낼 수 있다. 이 때 자식이 요구하는 크기가 ScrollViewer의 Extent Size가 되며, ScrollViewer의 가용 크기, 곧 화면에 노출되는 ScrollViewer의 크기가 Viewport Size가 된다. 따라서 다음이 성립된다.

  • Viewport Size : ExtentSize = Scroll Thumb Size  : ScrollBar Size

그리고 해당 정보를 사용해 VerticalScrollBar, HorizontalScrollBar의 Visual을 결정할 것이다. 그리고 이후 Scroll동작이 일어나면 ㅡ VerticalOffset또는 HorizontalOffset 값이 변경되면 ㅡ ScrollViewer의 Offset을 이용해 Child를 Arrange시키면 되는 것이다.

물론 Logical Scroll은 이와 다르게 동작한다. Logical Scroll을 위해서 ScrollViewer는 자신의 기능(Extent Size 결정, Viewport Size결정, Vertical Offset, Horizontal Offset이 변경되었을 때의 동작 등)을IScrollInfo라는 Interface를 구현하는 객체에게 위임해야 한다. 실제로 Logical Scroll로 동작하는 ScrollViewer는 ScrollBar를 표시하는 일만 하는 것 같다. 대부분의 일들을 ScrollViewer자신이 결정하지 않고 자신의 기능을 위임한 객체에게 맞긴다. UI Virtualization을 위한 Panel은 자식들의 실제 Physical Size를 알 수 없기에 반드시 이 Logical Scroll을 사용해야 한다.

이제 UI Virtualization을 위한 Panel의 동작을 살펴보자. 이 Panel의 동작은 ItemContainer의 생성, 화면에 보여지지 않는 ItemContainer의 파괴(Optional), ScrollViewer의 정보 설정, ItemContainer의 배치로 요약될 수 있다.

  1. ItemContainer 생성
  2. 화면에 보이지 않는 ItemContainer 파괴
  3. ScrollViewer의 정보 설정
  4. ItemContainer의 배치

1,2,3 동작은 Panel의 Measure동작시 이루어진다. Measure 동작시 Panel은 ScrollViewer 정보와 자신의 가용 크기를 바탕으로 화면에 보여져야하는 ItemContainer를 ItemContainerGenerator를 이용해 생성해야 한다. Vertical Orientation의 가상화 지원 StackPanel을 만든다고 한다면 아마 다음과 같은 코드를 사용할 수 있을 것이다.

protected override Size MeasureOverride(Size availableSize)
{
    _viewport = availableSize;
 
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
    if (itemsControl.Items.Count == 0)
    {
        return new Size();
    }
 
    double offset;
    GeneratorPosition position = ItemContainerGenerator.
                        GeneratorPositionFromIndex(FindItemIndexFromOffset(VerticalOffset, out offset));
 
    #region 영역의 크기만큼의 ItemContainer을 생성한다.
    using (this.ItemContainerGenerator.StartAt(position, GeneratorDirection.Forward, true))
    {
        double remainHeight = availableSize.Height
 
        while (remainHeight > 0)
        {
            bool isRealized;
            UIElement container = ItemContainerGenerator.GenerateNext(out isRealized) as UIElement;
            if (container != null && isRealized)
            {
                this.AddInternalChild(container);
                ItemContainerGenerator.PrepareItemContainer(container);
            }
            else if (container != null && !isRealized)
            {
                if (!Children.Contains(container))
                {
                    this.AddInternalChild(container);
                }
            }
            else
            {
                break;
            }
            container.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            remainHeight -= container.DesiredSize.Height;
        }
    }

위 작업이 종료되면 2,3번 동작을 MeasureOverride 메소드내에 포함시켜야 한다. 2번은 Optional할 수 있으므로 지나치더라도 3번 동작을 수행해야하는데 그 중에서도 ScrollViewer의 Extent Size를 결정해야한다. 그런데 이 ItemContainer의 크기가 고정적이라면 이것이 어렵지 않은데 각각의 ItemContainer 크기가 가변적이라면 어쩔 수 없이 "추정"이 들어가야 하므로 굉장히 복잡하고도 어렵다. 따라서 이 "추정"을 가능한 실재와 정확하게 하는 것이 가상화 Panel을 만드는 프로그래머의 주 목표라 할 수 있겠다. "추정"이 들어가는 부분은 이 뿐만이 아니다. 위 코드를 보면 FindItemIndexFromOffset이라는 메소드가 있는데 이 역시 ScrollViewer의 Vertical Offset을 바탕으로 가장 위에 보여질 Item의 Index를 "추정" 해야 한다.

protected override Size MeasureOverride(Size availableSize)
{
       // ...
       // ...
       // ...  
    this.extentSize = GetExtentSize();
    this.ScrollOwner.InvalidateScrollInfo();
    CleanUpItems(firstElement, lastElement);
    return availableSize;
}

현재 회사에서 만들고 있는 가상화 지원 패널은 ItemContainer의 크기를 바인딩 된 Data의 값을 바탕으로 얻을 수있기에 이 부분이 크게 어렵지 않았는데 이런 부가 정보가 없이 정확히 ScrollViewer의 정보를 계산하는 가상화 지원 패널을 만드는 것은 사실상 불가능하다.

이 Measure동작을 완료했다면 4.생성한 ItemContainer들을 배치하는 일이 남았다. 이 일은 ArrangeOverride 메소드가 담당해야 한다.

protected override Size ArrangeOverride(Size finalSize)
{
    double startYlocation =0.0;
    double offset;
    if (Children.Count == 0) return finalSize;
    var index = FindItemIndexFromOffset(VerticalOffset, out offset);
            
    GeneratorPosition position = ItemContainerGenerator.GeneratorPositionFromIndex(index);
            
    using (this.ItemContainerGenerator.StartAt(position, GeneratorDirection.Forward, true))
    {
        while (startYlocation < finalSize.Height)
        {
            bool isRealized;
            UIElement container = ItemContainerGenerator.GenerateNext(out isRealized) as UIElement;
            if (container == null) return finalSize;
            if (!Children.Contains(container))
            {
                throw new InvalidOperationException();
            }
 
            container.Arrange(new Rect(new Point(HorizontalOffset * -1, startYlocation),
                    new Size(finalSize.Width + HorizontalOffset,container.DesiredSize.Height)));
            startYlocation += container.RenderSize.Height;
        }
    }
    return finalSize;
}

여기까지 왔다면 ItemContainer들이 정상적으로 화면에 보이는 것을 확인 할 수 있을 것이다. 하지만 위 Measure와 Arrange코드는 사실 "최소한"의 부분이다. 사실 가장 중요한 부분은 역시 "추정"에 있는데 내가 만든 Panel은 그 동작이 현업의 특수한 상황에 의존적이라 이 코드에 추가하지 않았다. 완전한 코드를 소개하면 좋을 텐데 그러지 못해 조금 아쉽다. 그래서 다음 포스트에는 이 "추정"동작을 완벽 하게 해낼 수 없기에 이를 다른 방식으로 비껴나가는 전략을 취하는 Panel을 소개하기로 한다.

 

 

결론

보기에도 간단해 보이지 않는 이 가상화는 만들어진 소수의 ItemContainer를 바탕으로 보여지지 않는 영역을 예측, 추정해야 하는 것이기에 어려움이 있다. 때로는 "확률"이 쓰일 수도 있고 "평균"에 의지해야 할 수도 있다. 그리고 안타깝게도 이것은 결국 추정이기 때문에 어떤 경우에는 옳았던 구현이 다양한 상황에 의해 어떤 경우에는 틀릴 수 있다. 또한 모든 것을 이해한다고 생각하고 코딩에 뛰어들었지만 이 프로그램 세상은 언제나 그랬듯이 내가 미처 알지 못한 미지의 영역을 비웃으며 소개한다. 물론 그 미지의 영역을 헤집고 나아가는 것이 이 세계를 여행하는 자의 가장 큰 즐거움이라 할 수 있겠지만.

References :

신고

동료가 무엇인가를 붙잡고 있길래 보았더니 발상이 기발한 흥미로운 코드가 있었다. 정확히 기억이 나진 않지만 다음과 같았다.

   1: List<object> listA = new List<object>();
   2: listContainer.Where(l=>
   3:      {
   4:           listA.AddRange(l);
   5:           return true;
   6:      });
   7: //listContainer의 type은 List<<List<object>>이다.

그렇다. 그는 List<> 객체가 ForEach라는 메소드를 가지고 있는 것을 몰랐던 것이다. 그가 겪었던 문제는 Where를 ForEach로 바꾸는 것으로 간단히 해결되었다. 하지만 질문은 계속 되어야한다. 왜 동료가 작성했던 저 코드로는 원하는 결과를 얻을 수 없었던 것일까? 이 글을 읽고 있는 당신, 더 이상 읽지 말고 여기서 잠깐 생각해보시라!

대부분 정답을 바로 머리에 떠올렸을것이다. 부끄럽지만 난 2번 디버깅을 한 후에야 원인을 알았다.(어처구니 없게도 내 첫번째 추측은 C#컴파일러가 똑똑해 저 코드를 의미없다고 판단해 삭제시켜 컴파일 했으리라는 생각이었다.) 정답을 보자. Where<>는 IEnumerable<>을 반환하고 IEnumerable<>은 IEnumerator<>를 반환하는데 이 IEnumerator<>의 MoveNext()와 Current{}가 불린 적이 없기 때문에 Where절이 실행되지 않았던 것이다. 에잇! 뭐라는 거야! 열거자를 반환했는데 열거자가 열거되지 않아 Where()내의 lambda문이 실행되지 않았다는 것이다.(What Does the Yield Keyword Really Generate?글을 보면 깊은 곳까지 자세히 알 수 있다.) 즉 위 코드는 다음과 같이 쓰면 실행된다.

   1: List<object> listA = new List<object>(); 
   2: var results = listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }); 
   7: foreach(var result in results); 

물론 다음과 같이 쓸 수도 있다.

   1: List<object> listA = new List<object>(); 
   2: listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }).ToList();
   7:  

그런데 동료가 굳이 foreach를 사용하지 않고 Where메소드를 사용하려했던 이유는 IEnumerable<> 조합 + 확장 메소드 + lambda 조합으로 코드를 작성할때 손이 훨씬 덜가기 때문이리라.(추측임) 변수이름 치고 . 찍고 Where절 누르고 이러면 인텔리센스로 스피디 하게 훅훅 지나간다. 그리고 코드에 ()=> 이런거 보이면 나 왠지 코딩 좀 하는 것 같아 보인다. 여기에 맛들이면 언젠가 부터 코드에 foreach, for가 사라지기 시작한다. IEnumerable<>에 없는 ForEach, For는 자주 사용하는 Library dll의 필수 아이템이다.

   1: public static void ForEach<T>(this IEnumerable<T> list, Action<T> action)
   2: {
   3:     foreach (var item in list)
   4:     {
   5:         action(item);
   6:     }
   7: }

그런데 이게 좋은 습관은 아닌 것 같다. 일단 ForEach 확장 메소드를 쓰면 그 자리에 없었던 함수 호출의 수가 집합의 크기만큼 생겨버린다. 게다가 Action<T> delegate를 통한 호출이라 Inlining을 통한 최적화도 쉬울 것 같지 않다. 뭐... 사실 여기서 생긴 성능 감소는 알고도 억지로 눈감는 수많은 문제들에 비하면 너무 사소해 그냥 지나가도 양심에 부담감이 없지만 break 와 return 키워드들을 쓸 수 없다는 것이 큰 아픔이다.(ForEach내의 return문의 의미는 return이 아니라 continue이기에) 아니나 다를까 최근에 ForEach와 lambda문으로 조합된 코드를 foreach로 변경해야 할 일이 있었다. 코드를 처음 만들 때는 필요 없었는데 나중에 break가 필요했기 때문이다. 이 후에는 ForEach보다는 foreach를 사용하는데 사실 여전히 ForEach 타이핑의 편리함이 못내 그립다.

결론 : foreach, for를 사랑해주세요.

잡담 : 그런데 빌드시 C# 컴파일러는 코드 삭제를 통한 최적화는 하지 않는건가?

   1: List<object> listA = new List<object>(); 
   2: var results = listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }); 
   7: foreach(var result in results); 
   1: List<object> listA = new List<object>(); 
   2: listContainer.Where(l=> 
   3:      { 
   4:           listA.AddRange(l); 
   5:           return true; 
   6:      }).ToList();
   7:  

위 코드들 전부 최적화 옵션주고 컴파일 했을 때 허공으로 사라질 것이라 생각했는데 IL코드도 정상적으로 나오고 실행결과도 이전과 동일했다. 뭐, 열거자는 "열거"라는 의미가 있기에 저 코드를 의미없는 것이라 생각하지 않는구나! 라고 결론을 내리고 다음 코드로도 실험해보았는데

   1: for(int i=0;i<10;i++){}

이것도 정상적으로 IL 코드에 추가되더라. 그래서 C# 컴파일러는 코드 제거를 통한 최적화는 하지 않는다는 결론을 내렸다.(최적화 옵션을 주고 빌드했을 때 바뀐점은 첫번째 예의 코드내의 foreach 키워드가 while로 변한것이었다. 여기서도 foreach는 사랑받지 못한다. ㅜ.ㅜ). 아 그리고 .NET 프로그램은 2번 최적화 기회를 가지는데 IL 코드로 변환될 때가 첫번째요, IL코드가 Native코드로 변환되는 JIT 컴파일 타임이 두번째이다. 그런데 Language Compile Time보다 JIT Compile Time에 더 많은 최적화가 이뤄진다고 한다.(좀 의아하다.) 마지막 'for'문의 예는 IL 코드에 추가되긴 했지만 JIT Compile Time에 IL 코드 삭제를 통한 최적화가 이루어졌을 수도 있기 때문에 실제로 실행되었는지 여부는 알 수 없다.(이걸 알 수 있는 방법은 없나?)

--- 있었다. JIT 컴파일러가 생성한 기계어 확인하는 법을 보면 확인 할 수 있다. 정책임님 감사해요~^^

references : Release IS NOT Debug: 64bit Optimizations and C# Method Inlining in Release Build Call Stacks, What Does the Yield Keyword Really Generate?

신고
TAG forEach

WPF는 극복해야한다.

WPF
가 극복해야 하는 그것은 사실 WPF만이 가지는 고유한 것이 아니다. 그것은 모든 프레임워크라 불리는 것들이 극복 해야 하는 일종의 원죄와도 같은 것이다. 그리고 그 원죄는 그것을 사용하는 개발자가 아닌 그것을 만든 창조자가 깨끗이 씻어 구원의 길로 이르게 해야 한다. 다시 말한다. 창조자들은 그 문제를 반드시 해결해야만 한다.

그렇다면 그 원죄란 것이 무엇인가? 조엘은 이 원죄를 The Law of Leaky Abstractions라는 멋진 글로 잘 정리해놓은 바 있다. 바로 '추상화'에서 오는 side-effect인 것이다.

 

So the abstractions save us time working, but they don't save us time learning.

 

And all this means that paradoxically, even as we have higher and higher level programming tools with better and better abstractions, becoming a proficient programmer is getting harder and harder.

, 역설적이게도 높은 추상화의 프로그래밍 툴을 가지고 있다고 하더라도 숙련된 프로그래머가 되는 것은 오히려 점점 더 어려워진다. 또한 추상화는 프로그래머가 일하는 시간을 절약해 주지만 그것은 결코 프로그래머의 배움의 시간을 절약해주지는 않는다. 더 나아가보자. 높은 추상화 레벨의 프로그래밍 툴(도구와 프레임워크)을 사용해야 할 때 개발자가 알아야만 하는 지식의 양은 오히려 증가한다. 우리는 배워야 한다. 알고리즘이 아닌, 컴퓨터 사이언스가 아닌, 심지어 C#언어도 아닌 그 WPF를 별개로 배워야 한다. 그것만의 철학과 그것만의 독자적 개념과 그것만의 언어를 따로 배워야 한다.

 

따라서, 훌륭한 프레임워크는 첫째, 다음과 같이 말할 수 있어야 한다.

이 프레임워크를 배우기 위해 투자한 당신의 시간이 절대 헛되지 않을 것입니다.”

 

프레임워크의 창조자는 위 문장을 개발자들이 의식적이든 무의식적이든, 사실로 느낄 수 있도록 해야 한다. 개발자들이 의식적으로 분명히 느낄 수 있도록 프레임워크는 우리에게 그 배움의 가치가 프레임워크의 목적 생산성의 향상을 가져다 줄 것이라는 사실을 자세하고도 측정 가능한 수치로 우리에게 제시해야만 하고 개발자들이 무의식적으로 그 프레임워크가 곧 대세가 될 것이라는 사실, 고로 이 프레임워크를 배우는 것이 네게 이득을 가져다 줄 것이라고 세뇌시켜야 한다.

WPF는 이 작업에 성공했는가? 난 이 질문에 대한 답을 하지 않겠다. 왜냐면 당신이 WPF에 대해 느끼는 이미지가 바로 그것의 대답이므로.


둘째, 좋은 프레임워크는 기존의 개발자들이 쉽게 배우고 사용할 수 있도록 좋은 프로그래밍 경험을 가지고 있어야 한다. 어렵게 말하지 말자. 사용하기 쉬워야 한다는 것이다. 가장 좋은 것은 그것의 독자적인 언어와 도구 그것이 그것만이 가지는 개념을 잘 표현할 수 있도록 하는 것이다. 또한 배우기 쉽도록 다양한 예제와 서적을 제공해 주어야 한다.

WPF는 이 작업에 성공했는가? . WPF는 젠장, (깊이) 배우기 쉽지 않다. 굉장히 웃긴 것은 WPF GUI프로그래밍 미 경험자보다 기존의 Win32 WinForm을 해오던 개발자들이 저절로 알아내기 힘든 개념을 참 많이 가지고 있다는 것이다. 이것은 기존에 알던 지식이 새로운 배움에 오히려 해가 될 수 있다는 것을 의미하며, 이것은 기득권에 반하는 것이다. 참으로 WPF의 색은 너무나 붉으며 그가 서있는 장소는 너무 왼편이다.


셋째, 좋은 프레임워크는 그 자신을 두꺼운 외투로 감추는 것이 아니라 안이 훤히 비치는 시스루 옷을 입고 나타나야 한다. 즉 그것이 어떠한 문제를 해결하기 위해 출발했고 그것이 그 문제를 어떠한 방법으로 해결 했는지, 반드시 우리에게 최대한 자세히 보여야 한다. 추상화에서 오는 Side-Effect를 극복하는 방법이 바로 여기에 있기 때문이다.

WPF는 이렇게 하고 있는가? . 그리고 바로 이 지점에서 내 불만이 폭발한다. , 추상화의 문제가 바로 극명하게 나타나는 것은 대부분 성능의 문제에서 비롯된다. 새 프레임워크를 들여왔다. 생산성이 폭발한다. 열심히 무엇인가를 만들어냈다. 그런데.. 점점 느려진다. 이것을 해결하려면 개발자가 만든 프로그램이 아닌, 그 프로그램의 토대, 곧 그 프레임워크 내부의 동작과 원리를 자세히 알아야 한다. 우리는 그 정보를 쉽게 얻을 수 있는가? 내 대답은 긍정적일 수 없다. 가뜩이나 추상화의 옷들을 몇 겹이나 뒤집어쓰고 있는 WPF인데 그 옷 하나 하나 벗겨내기가 너무 힘들다. 물론, 어떤 이들은 그 옷들을 기어이 벗겨 낼 것이고 성인의 반열에 들어 권력을 얻게 될 것이다. 하지만, 이것은 근본적인 방법이 아니다. 글 첫머리에 언급했듯이 이것은 창조자가 제공해야 하는 일이다.

 

여러 겹의 추상화의 옷을 입으려면 그 옷 하나하나가 투명해, 그 속을 훤히 드러낼 수 있어야 한다. 어렵다고? 그 누구도 그렇게 하고 있지 않다고? 그것은 내 알바아니다.

신고

WPF를 생각한다.

프로그래밍 2010.08.11 00:46 Posted by 아일레프

고작 2년 남짓 경력을 가지고, 이제야 눈을 가리고 있던 장막이 조금 걷히는 나로써 이 커다란 코끼리를 얘기 한다는 것이 우습지만. 감히 WPF에 대한 이야기를 하려 한다. 어쩌면 길지도 모르는 이 글을 읽기 전에 전에 알아야 할 사항이 있다. 난 약 2년 동안 .NET 프레임워크 위에서 WPF만 가지고 놀았다. 그리하여 WPF가 이제 내게는 둘도 없는 친한 친구이다. 따라서 이에 대한 이야기가 아무래도 중립적일 수 없다. 스스로 아무리 객관적으로 판단하려고 애를 써도 어느 정도 애정이 들어가기 때문이다. 따라서 이 글 내에서 WPF의 긍정적인 부분은 어느 정도 과장되고, 부정적인 부분은 축소될 수 밖에 없는 운명에 있다는 것을 밝힌다. 그리고 그 부분을 고려해 WPF에 대해 판단하는 것은 이 글을 읽고 있는 그대의 몫이다.

 

 

 

View의 것은 XAML에게, 논리의 것은 .cs에게

 

응집력. 프로그래머들이 클래스를 설계하고 리팩토링할 때 반드시 고려해야 하는 것 중 하나다. 이 원칙에 의해 UI 어플리케이션에 포함되는 클래스를 설계할 때 성숙한 개발자라면 반드시 View와 논리를 분리를 고려해 클래스를 설계할 것이다설계를 마친 후 그 클래스를 구현할 시점에 느끼게 되지만논리를 구성하고 만드는 일은 참 재미있는 일이다.(나에게는 그러했다.) 그러나, 단순한 노가다 처럼 느껴지는 View를 만드는 작업은 너무 귀찮고 그 귀찮음이란 너무 고통스럽다. UI Application의 역사를 보면 이와 같은 생각을 가진 지독히 게으른 개발자들의 고뇌를 읽을 수 있다. 그들이 고안한 다양한 시도들에 대해서는 여러분도 익히 알고 있을 것이다. 그리고 감히 말하건데, 그 시도들은 WPF에서 화려한 꽃을 피운 것으로 보인다. View는 이제 온전히 XAML의 것이 되었다. 어느 정도 숙달된 WPF개발자라면 더 이상 .cs코드에 new Button(); 등으로 UIElement요소를 직접 생성하지 않아도 됨을 알 수 있을 것이다.(만약 현재 당신의 WPF코드 내에 new 키워드로 UIElement를 생성하고 있다면 부지런히 다른 방법을 고려 해봐야 한다.) 

이렇게 View가 온전히 XAML의 것이 되었다는 것은 무엇을 의미하는가? 바로 디자이너와 개발자의 온전한 분업이 가능해졌다는 것을 의미한다.('가능'이란 단어를 택했다는 것에 주의하기 바란다.) 고로 View의 것은 디자이너에게, 논리의 것은 개발자에게.

 

하지만, 문제는 남아있다. View의 것이 완전히 디자이너의 영역에 도달하려면 훌륭한 XAML툴이 필요하다이제 우리에게 주어진 것이 오직 Blend라는 프로그램뿐이라는 비극적인 현실이 기다리고 있다디자이너들에게 이 Blend만을 사용해 멀쩡한 View가 태어나기를 바라는 것은 터무니 없는 욕심이다. 이 툴이 얼마나 가혹하냐면, 개발자도 알기 어려운 에러메시지를 뿌리며 빌드가 되지 않는 것은 흔히 벌어지는 일이며, 배 깔고 발 뻗고 죽어버리는 일도 허다하다참으로 이 Blend란 툴은 애물단지이다.


내가 디자이너가 아니기에 확신할 수는 없지만, 개발자가 논리에 집착하듯이 디자이너는 아름다운 디자인에 자신의 에너지를 바친다. 그런데 그 디자이너는 틈만나면 죽는 이 Blend프로그램 때문에 알 수 없는 에러메시지를 봐야 하고, 그 때 마다 미안한 마음을 드러내며 개발자에게 Blend를 살려달라고 긴급요청을 해야 한다. 더 큰 문제는 고객의 요구가 이 Blend로 구현할 수 있는 XAML의 영역을 넘어서기에, 고객의 그 어마어마한 창의력을 현실에 옮기기 위해서, 혹은 디자이너의 머리 속에 있는 그 이상을 현실로 옮기기 위해서어쩔 수 없이 디자이너가 어느 정도 XAML을 이해 해야 하는 처지에 몰린다는 것이다. 아름다움의 세계에 머물고 싶은 디자이너들에게 이 요구는 너무 잔인하다. 때문에 View를 구성하기 위해서는 WPF개발자 한 명과 디자이너 한 명이 필요하다. 혹은 최소한 WPF XAML을 이해하고 있는 디자이너 한 명이 필요하다. 하지만 WPF의 XAML을 읽을 수 있는 디자이너는 소수이다.(귀동냥으로 들은 사실) 이 즈음에 우리 회사에 있는 두 명의 디자이너에게 감사하다는 말을 전하고 싶다. 이 분들은 XAML을 이해한 후 개발자에게 적절한 요구를 할 뿐 아니라 때에 따라서 디자이너에게는 너무 멀리 있는 Visual Studio 프로그램을 손수 연 후 .cs 코드를 직접 만지는 수고를 감내하기도 한다. 신이여 그녀들에게 축복을!

 

WPF 기술은 어느 정도 완성되었다. 그렇지만 Blend는 그 기술을 따라잡기에 너무 벅차며, 그 기술을 따라잡고자 하는 노력이 부족하다는 것에 내 분노의 뿌리가 있다.(Blend4는 WPF4 지원한다고 하면서 x:TypeArguments, x:Reference 같은 XAML 2009문법을 이해하지 못한다.)  때문에 개발자와 디자이너와의 완전한 분업이란 Blend만 존재하는  현실에서 환상이다.

  

 

View의 것은 View에게, 논리의 것은 ViewModel에게. Binding. 그 놀라운 힘.

 

언젠가 문득 UserControl .cs클래스 내에 this.DataContext= this; 또는 UserControl XAML내에 DataContext=”{Binding RelativeSource={RelativeSource Self}” 란 코드를 넣고 싶다고 느낀다면, 그대는 이미 MVVM패턴을 어느 정도 하고 있는 것이다..

 

Binding 바로 이 엄청난 녀석으로 인해 우리는 비로서 논리의 것과 View를 느슨히 연결 시킬 수 있게 되었다. 그리고 이것으로 인해 UI의 특정 동작을 일으키는 지우고 싶은 수많은 코드들을 저 세상으로 보낼 수 있게 되었다.(ex: textBlock1.Text=”dfsdf”) 이것은 View관리 코드(ViewModel)에서 특정 UIElement reference하는 코드가 더 이상 보이지 않게 됨을 의미한다. 이전에 View관리 코드와 View사이의 통신이 Event로 강하게 연결되어 있었다면 이제 Binding으로 느슨히 연결되어 응집력 있는 클래스의 설계와 구현이 가능해졌기 때문이다. 그 뿐 아니라 이것은 ViewModel– View관리 논리- 을 별도로 테스트 할 수 있음을 의미하기도 한다. 진부한 문장이겠지만 적지 않을 수 없다. 유지보수하기 편해졌다.

 

그러나, Binding만으로 통신할 수 없는 경우가 분명히 존재한다. 때에 따라서는 Event를 사용해야 할 경우가 분명히 발생한다는 것이다. 이 때 MVVM으로 View를 구성하는 개발자는 알 수 없는 죄책감을 애써 뿌리치며 UserControl .cs코드에 손을 데야 한다. 이 경우 AttachedProperty를 사용하는 등의 고급 기술(ex: BehaviorCommand, TreeView의 SelectedItem TwoWay Binding)로 해결 할 수는 있지만 Binding으로 정복되지 않은 영역이 아직 더 많다. 물론, View cs코드로 View의 세부 동작을 컨트롤 하는 것으로는 느슨한 결합이란 원칙이 깨지지 않는다. 하지만 View 에서 ViewModel을 직접 참조한다던가(이것만으로는 나쁘지 않을 수도 있다), ViewModel에서 View의 특정 UI Element를 참조한다면, 완벽한 느슨한 결합이란 물 건너 간 것이다. 이를 해결하기 위해서는 WPF개발자는 적지 않은 시간을 쏟아 내야 한다. 그리고 그 적지 않은 시간의 열매가 문제의 해결을 보장하지 못한다는 사실에 비극이 있다.

 

또한 Binding 그 자체에서 오는 문제도 분명히 존재한다. Binding으로 느슨한 결합이 완료 되었는지를 확인하려면 실제로 프로그램을 실행해 봐야 한다. , Binding Path에 오타를 적었다고 해도, RelativeSource에 자기가 생각한 Source가 매칭되지 않았다 할 지라도 CompileTime에 에러를 낼 수 있는 방법이 현재로선 없을뿐더러, 런타임에도 Exception을 발생시키지 않는다. 가장 좋은 해법이 Debug Output창에 에러 메시지를 확인 하는 뿐이다. 요는, ViewModel 자체를 별도로 테스트 할 수는 있지만 - 내 경험에서 발견된 사실이지만 - UI동작의 버그는 ViewModel자체에서 뿐 아니라 View ViewModel사이의 연결 때문에(Binding 때문에) 많이 발생 되는데, 이것을 별도로 테스트 하거나 Runtime전에 확인할 방법이 없다는 것이다.

 

또한 MVVMWPF개발자에게 생각보다 많은 WPF의 이해력과 통찰력을 요구한다. 그리하여(당연하겠지만) 이 멋진 디자인 패턴을 이해하고 완전히 적용하려면 적지 않은 노력이 필요하다.

 

 

 

--- 이 후에 계속됩니다

저작자 표시
신고
TAG Blend, MVVM, WPF

Canvas supports ScrollViewer.

프로그래밍 2010.07.15 17:45 Posted by 아일레프

 <ScrollViewer Height=100>

          <Canvas>

                   <TextBlock Canva.Top=120 Text=Test/>

          </Canvas>

</ScrollViewer>

 

VerticalScrollBar is visible in upper case? Unfortunately, it is invisible, because the Size of Canvas is zero-zero.

How we can see the VerticalScrollBar in upper case? 

The problem is caused by MeasureOverride method of Canvas. Below is an original MeasureOverride method of Canvas.

protected override Size MeasureOverride(Size constraint)

{

    Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

    foreach (UIElement element in base.InternalChildren)

    {

        if (element != null)

        {

            element.Measure(availableSize);

        }

    }

    return new Size();

}

OK, then, solution can be very easy, just define new Canvas, and override MeasureOverride method like below.

        protected override Size MeasureOverride(Size constraint)

        {

            Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

 

            double maxHeight = 0;

            double maxWidth = 0;

 

            foreach (UIElement element in base.InternalChildren)

            {

                if (element != null)

                {

                    element.Measure(availableSize);

                    double left = Canvas.GetLeft(element);

                    double top = Canvas.GetTop(element);

                    left += element.DesiredSize.Width;

                    top += element.DesiredSize.Height;

 

                    maxWidth = maxWidth < left ? left : maxWidth;

                    maxHeight = maxHeight < top ? top : maxHeight;

                }

            }

            return new Size { Height = maxHeight, Width = maxWidth };

        }

 

Thank you,

 

** Thinking Outside the Grid 
refered
referred the difference between Grid and Canvas.

The difference between the Canvas and the single-cell Grid is in how the container appears to the rest of the layout system. WPF and Silverlight incorporate a two-pass, top-down layout where every element interrogates the size of its children and is then responsible for arranging its children relative to itself. Within this layout system, the Canvas and the single-cell Grid are very different:
1. To its children, the Grid has the same dimensions as the dimensions of its own parent. These are usually finite dimensions, but the Canvas always appears to have infinite dimensions to its children.
2. The Grid reports the composite size of its children to its parent. 
However, the Canvas always has an apparent size of zero, regardless of the children it contains.

저작자 표시
신고

TwoWay Binding of SelectedItem in TreeView

프로그래밍 2010.07.14 19:22 Posted by 아일레프

Hello guys, I’m a elementary WPF programmar and elementary English writer. J  And this is my first English article. I think you are fortune, because you can see that someone start something.  

OK, now I start.

SelectedItem of TreeView does not support two-way binding(It is readonly) and I don’t know why(someone knows it?) . I searched this topic, but I couldn’t find any good solution.  

I had used to SelectedItemChanged event and maually set the IsSelected of TreeViewItem has SelectedItem as It’s DataContext.

I’m a really lazy person, but I can’t take it anymore. Finally, I found an easy & good(really?) solution.

Now, I can bind SelectedItem of TreeView like this way.

 

<TreeView x:Name="mainTreeView"  ItemsSource="{Binding TemplateService.SelectedTemplate.Regions}"    

                                             illef:TreeViewExtension.SelectedItem="{Binding TemplateService.CurrentSelectedRegion}">

I want to introduce how can I make it.

First of all, I need a helper function return a TreeViewItem has SelectedItem as it’s DataContext. You can use a below method. (This function was suggested by Mr. Sys.pe.kr)  

       public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)

        {

            TreeViewItem containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);

            if (containerThatMightContainItem != null)

            {

                return containerThatMightContainItem;

            }

            else

            {

                return ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);

            }

        }

 

        private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)

        {

            foreach (object curChildItem in itemCollection)

            {

                TreeViewItem parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(curChildItem);

                if (parentContainer == null)

                {

                    continue;

                }

                TreeViewItem containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);

                if (containerThatMightContainItem != null)

                {

                    return containerThatMightContainItem;

                }

                TreeViewItem recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);

                if (recursionResult != null)

                {

                    return recursionResult;

                }

            }

            return null;

        }

And I need a class define attached property.

    public static class TreeViewExtension

    {

          public static readonly DependencyProperty SelectedItemProperty =

                    DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewExtension),

                             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemIsChanged));

 

        public static void SetSelectedItem(TreeView treeView, object value)

        {

            treeView.SetValue(SelectedItemProperty, value);

        }

        public static object GetSelectedItem(TreeView treeView)

        {

            return treeView.GetValue(SelectedItemProperty);

        }

        private static void SelectedItemIsChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

                   //TO DO:?

        }

       }

You can guess what will be placed in “TO DO”. Who is the sender? Yes, sender is TreeView and e.NewValue is new selectedItem value.

So, I placed below job in there.

        private static void SelectedItemIsChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

                   SetSelectedItem(e.NewValue);

                   TreeView treeView = sender as TreeView;

                   treeView.SelectedItemChanged += (o,e)=> SetSelectedItem(e.newValue);

                   if(treeView.SelectedItem != GetSelectedItem(treeView))

                   {

                             treeView.ContainerFromItem(GetSelectedItem(treeView)).IsSelected=true;

                   }

        }

I think you can easly know what I want to do. However, upper method is so ugly. Why? First, it doesn’t work J Second, It will make memory leak. The static class must do not handle any UIElement’s events.

In these case, there are two solutions I found.

First, use WeakEvent. This is greate solution, but there is problem it is hard to implement WeakEvent. See this CodeProject site. – Weak Event in C#

Second, my favorite Watcher-Register Pattern, I named it. J

1)    Made Watcher class. That will handle UIElement’s events.

2)    Store it in UIElement’s another attached property.

Made Watcher class.

    public class SelectedItemWatcher

    {

        private TreeView treeView;

 

        public SelectedItemWatcher(TreeView treeView)

        {

            this.treeView = treeView;

            if (treeView.SelectedItem != null)

            {

                TreeViewExtension.SetSelectedItem(treeView, treeView.SelectedItem);

            }

            treeView.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeView_SelectedItemChanged);

        }

        void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)

        {

            TreeViewExtension.SetSelectedItem(treeView, treeView.SelectedItem);

        }

        TreeViewItem clearedTreeViewItem;

        public void SetValue(object value)

        {

            if (value == null)

            {

                TreeViewItem selectedTreeViewItem = treeView.SelectFirstObject<TreeViewItem>(s => s.IsSelected);

                if (selectedTreeViewItem != null)

                {

                    treeView.PreviewMouseLeftButtonDown += SelectedTreeViewItem_PreviewMouseLeftButtonDown;

                    selectedTreeViewItem.IsSelected = false;

                    clearedTreeViewItem = selectedTreeViewItem;

                }

            }

            if (treeView.SelectedItem != value)

            {

                treeView.PreviewMouseLeftButtonDown -= SelectedTreeViewItem_PreviewMouseLeftButtonDown;

                TreeViewItem treeViewItem = TreeViewExtension.ContainerFromItem(treeView, value);

                if (treeViewItem == null)

                {

                    return;

                }

                treeViewItem.IsSelected = true;

            }

        }

        private void SelectedTreeViewItem_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)

        {

            DependencyObject dependencyObject = e.MouseDevice.DirectlyOver as DependencyObject;

            TreeViewItem treeViewItem = dependencyObject.FindParentInVisualTreeNode<TreeViewItem>(20);

            if (treeViewItem == clearedTreeViewItem)

            {

                treeViewItem.IsSelected = true;

                treeView.PreviewMouseLeftButtonDown -= SelectedTreeViewItem_PreviewMouseLeftButtonDown;

            }

        }

    }

Store it as another Attached Property in TreeView.

          public static readonly DependencyProperty SelectedItemProperty =

                    DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewExtension),

                             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemIsChanged));

        //For storing Watcher Class

        private static readonly DependencyProperty SelectedItemWatcherProperty = DependencyProperty.RegisterAttached("SelectedItemWatcher", typeof(SelectedItemWatcher), typeof(TreeViewExtension));

        #region GetValue & SetValue

        public static void SetSelectedItemWatcher(TreeView treeView, object value)

        {

            treeView.SetValue(SelectedItemWatcherProperty, value);

        }

        public static SelectedItemWatcher GetSelectedItemWatcher(TreeView treeView)

        {

            return (SelectedItemWatcher)treeView.GetValue(SelectedItemWatcherProperty);

        }

        private static void SelectedItemIsChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

            TreeView treeView = sender as TreeView;

            Debug.Assert(treeView != null);

 

            if (TreeViewExtension.GetSelectedItemWatcher(treeView) == null)

            {

                TreeViewExtension.SetSelectedItemWatcher(treeView, new SelectedItemWatcher(treeView));

            }

            else

            {

                TreeViewExtension.GetSelectedItemWatcher(treeView).SetValue(e.NewValue);

            }

        }

In upper case, You can handle UIElement’s event without any memory leak.

OK, Thank you for your reading J I attached my TreeViewExtension.cs contains upper classes.

Have a good day!

저작자 표시
신고

Left Shift + Right Shift + 마우스 클릭

프로그래밍 2010.05.31 14:37 Posted by 아일레프

두 눈을 비비고 다시 보았다. 분명히 저렇게 써있다.

 

메뉴를 Left Shift Right Shift를 누른 상태에서 클릭하면 새 창으로 해당 메뉴의 항목이 열리게 할 것

 

새 창으로 메뉴를 열려면 손이 꽤나 커야한다.

 

 

“IPhone처럼 스크롤 뷰 내에서 마우스 Drag 로 스크롤을 움직이게 해주세요

 

여기 저기서 본 것은 많아 좋은 것은 모두 넣으려고 한다. 조금만 더 생각 해보면 마우스 Drag로 스크롤 뷰를 움직이려는 시도가 형편없는 것임을 알게 될 터인데.

 

 

 

목놓아 외친다해도 들릴것 같이 않으니 별 수 없다. 

---------------여기 까지 2010.05.26일 적은 내용---------



---------------2010. 5. 31 일 업데이트 내용 --------------

Left Shift + Right Shift + 마우스 클릭이라는 잔인한 기능 명세에는 또 다른 할말이 없지만, 두 번째 "IPhone처럼 스크롤 뷰 내에서 마우스 Drag로 스크롤을 움직이게 해달라는 것"에 대한 내 생각이 잘못되었음이 밝혀졌기에 적는다. 충분히 멋지게 구현할 수 있더라. ㅋ

--> 때로 고객의 요구는 조금 더 생각하면 "형편없는 것"이지만 아주 많이 생각하면 내 성장을 도울 수 있는 무언가이다.

저작자 표시
신고

Simple WPF Tip : Is A in bound of B?

프로그래밍 2010.03.29 10:37 Posted by 아일레프

이 포스트는 Is it possible to find out if an Item is currently hidden because it exceeds the bounds of real estate given to it? 의 내 답글을 옮긴 것이다.

 

Q : As the title states, I'm wondering if it's possible to determine if a particular UI element is currently hidden because it's out of bounds.  For example, if an item is located within a ScrollViewer, and it is below the scrollable region currently visible, can you query that somehow?

 

My Answer :

Visual 객체의 TransformToVisual 메소드를 이용해 다음과 같이 Bound 여부를 알아 낼 수 있겠다.
From” 객체와 “To” 객체의 2차원 공간에서의 Rectangle을 구하고, Rectangle Intersect의 존재여부를 검사한다. 만약 Intersect가 존재한다면 “To is in bound of From”인 것이다.

 

public static bool IsInBound(this FrameworkElement from, FrameworkElement to)

        {

            MatrixTransform transform = to.TransformToVisual(from) as MatrixTransform;

 

            Rect rect = Rect.Intersect(new Rect(new Point(0, 0),

                                                        new Point(from.ActualWidth, from.ActualHeight)),

                                            new Rect(new Point(transform.Value.OffsetX, transform.Value.OffsetY),

                                                        new Point(to.ActualWidth + transform.Value.OffsetX,

to.ActualHeight + transform.Value.OffsetY)));

 

            if (rect.Equals(Rect.Empty))

            {

                return false;

            }

            else

            {

                return true;

            }

        }

 

혹시나 편한 Helper가 이미 존재한다면 GG

저작자 표시
신고

Social MSDN Forum 활동

프로그래밍 2010.03.10 06:52 Posted by 아일레프

어떤 Community에도 정착하지 못하다가 현재 Social MSDN Forum에서 활동하고 있다.

 

Activity Rss는 다음과 같다.

http://services.social.microsoft.com/Feeds/Activities?user=illef&lcid=en-US&brand=Msdn&format=rss20

 

그리고 정말 유익한 Dr.WPF Activity Rss도 소개한다.

http://services.social.microsoft.com/Feeds/Activities?user=Dr.%20WPF&lcid=en-US&brand=Msdn&format=rss20


저작자 표시
신고

2010.3.8 내용 추가.

x:Class
를 이용한 ResourceDictionary 동적 생성의 또 하나의 응용으로 Storyboard - Completed Event problem 가 있습니다.

 
 Storyboard - Completed Event problem 단지 Storyboard에 대한 예였으나 이를 “Resource에 추가한 객체에 이벤트를 추가시키는 법으로 일반화 시켜서 생각해 볼 수 있겠습니다.

-------------------------------------------------------------------------------------------------------

이것은 그냥 간단한 팁입니다.

WPF로 프로그래밍을 하다 보면 간혹 동적으로 ResourceDictionary를 사용하고 싶을 때가 있습니다. 런타임상에 별도의 ResourceDictionary를 로드해 해당 Dictionary의 특정 Key에 해당되는 Value를 사용하고 싶다는 것이지요. 이럴때 보통 XamlReader를 이용해 ResourceDictionary.xaml을 Load한 뒤 해당 ResourceDictionary로 캐스팅 해서 사용하게 되는데요, 이런 목적이라면 x:Class를 활용하는 것이 더 괜찮을 수 있습니다.

먼저 ResourceDictionary를 프로젝트에 추가합니다.

 

그리고 동일한 이름으로 '동일한이름.xaml.cs'를 추가합니다.

아래와 같이 Project에 페어링 된 형태로 추가된 것을 확인할 수 있습니다.

이를 확인하고 Dictionary.xaml.cs를 다음과 같이 수정합니다.

public partial class Dictionary : ResourceDictionary
{
    
public Dictionary() 
    
{
         
InitializeComponent(); 
     }
 
}

이 때 InitializeComponent 메소드가 존재하지 않기 때문에 Error List에 InitializeComponent메소드가 존재하지 않는다는 에러가 추가됨을 확인할 수 있습니다.

InitializeComponent메소드를 만들어 줘야 합니다. 어떻게 만들어 주냐? Dictionary.xaml에 X:Class를 추가해주면 됩니다.

<ResourceDictionary

x:Class="HierarchicalDataGridTest.Dictionary"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

 

</ResourceDictionary>

빌드가 성공함을 알 수 있습니다. 이제 코드에서 ResourceDictionary dic = new Dictionary(); 이와 같이 ResourceDictionary를 손쉽게 만들어 사용할 수 있습니다.

좀 더 나가면 이렇게 만든 Dictionary는 WPF Component Repository의 역할을 수행할 수도 있습니다.

public partial class Dictionary : ResourceDictionary

    
public Dictionary() 
    

         
InitializeComponent();
    

    
public Style MyButtonStyle 
    

         
get { return this["MyButton"] as Style; } 
    
}
}

신고

Using a ViewModel as a value converter

프로그래밍 2010.02.25 18:24 Posted by 아일레프

<TextBlock Background="LightBlue" Text="{Binding Path=Text}" TextAlignment="Center"

                   Width="{local:BindingEx Binding={Binding ElementName=window, Path=ActualWidth}, ConverterMethod=AdjustTextWith}"/>

 

MVVM Pattern을 사용하면서 부딛히는 문제 중 하나는 많은 수의 Converter의 필요에서 오는 귀찮음과 비효율성이다. 컨버터가 필요할 때마다 별도의 클래스를 만들어 주어야하며, Resource에 추가해야 했으며, StaticResource MarkupExtension으로 사용해야 했다. 이전에 이것에 대한 해법으로 ConverterManager라는 글을 쓴 바 있지만 이것은 Converter Resource에 중복 생성하는 것에 대한 문제를 해결 했을 뿐, 많은 수의 Converter 클래스를 만들어야 한다는 비 효율성을 제거하지는 못했다.

 

이전에 소개한 LambdaValueConverter의 경우 LambdaConverter MarkupExtension을 사용해 XAML내에서 Expression을 사용 할 수 있게끔 했다. 여기에 이어 이 문제를 해결하기 위한 새로운 접근 방법을 소개한다. ViewModel의 특정 메소드를 Converter로 쓸 수 있게 하는 방법이 그것이다. John Smith on WPF 에서 확인 할 수있다.


저작자 표시
신고

LambdaValueConverter

프로그래밍 2010.02.23 18:09 Posted by 아일레프
Binding Lambdas1


맙소사, Converter={AvalonLamdas:LamdaValueConverter (param/2)-20}}이라니, 놀랍지 아니한가? Converter내에 수식을 넣을 수 있는 것이다. 이제야 발견하고, 이런 걸 만들 생각조차 하지 않았다는 것이 부끄럽다.(해당 Converter는 벌써 2008년에 만들어진 것이다.) Dynamic Expression API 의 한가지 쓰임이라고 할 수 있는데 유용하게 사용 할 수 있을 것 같아 이곳에 소개한다.

 

Embed code in XAML 페이지를 통해 확인할 수 있다.

저작자 표시
신고

WPF Template의 이해

프로그래밍 2010.02.09 19:30 Posted by 아일레프

Template에 대해서 간단히 포스팅 합니다.

 

<Button Height="100" >

            <TextBlock Height="Test"></TextBlock>

</Button>

 

XamlReader는 위 XAML 코드를 아래와 같은 코드로 번역할 것입니다.

 

Button button = new Button { Height = 100 };

button.Content = new TextBlock { Text = "Test"

 

Button과 TextBlock의 실제 Instance를 내부에서 실제로 ‘생성’하게 되는 것이죠. 하지만 Template의 경우는 이와 완전히 다릅니다.

 

<Button Height="100" >

            <Button.ContentTemplate>

                <DataTemplate>

                    <StackPanel Orientation="Horizontal">

                        <TextBlock Text="Content : "/>

                        <ContentControl Content="{Binding }"/>

                    </StackPanel>

                </DataTemplate>

            </Button.ContentTemplate>

            <TextBlock>Test</TextBlock>

</Button>

 

그리고 이 포스트를 더 읽지 마시고, 잠깐 저 XAML을 코드로 나타내면 어떻게 될 지 생각 해봅시다.

 

 

 

혹시 아래와 같다고 생각하셨나요?

               
Button button = new Button { Height = 100 };

DataTemplate template = new DataTemplate();

StackPanel stackPanel = new StackPanel { Orientation = Orientation.Horizontal };

stackPanel.Children.Add(new TextBlock { Text = "Content : " });

stackPanel.Children.Add(new ContentControl { Content = new Binding() });

template.Content = stackPanel;

 

button.ContentTemplate = template;

button.Content = new TextBlock { Text = "Test" };

 

하지만 재미있게도 Template의 경우는 위와 같은 코드로 번역되지 않습니다. 왜냐면 Template – 한국말로 ‘틀’ – 이기 때문입니다. Template의 목적은 하나의 단일 Object Tree를 생성하기 위한 것이 아니라 Object Tree를 만들어내는 ‘틀’을 만드는 것에 있기 때문입니다. 긴말하지 않고 위 XAML이 어떻게 번역되는지 코드로 알려드립니다.

 

Button button = new Button { Height = 100 };

FrameworkElementFactory stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));

stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);

 

FrameworkElementFactory textBlockFactory = new FrameworkElementFactory(typeof(TextBlock));

textBlockFactory.SetValue(TextBlock.TextProperty, "Content : ");

 

FrameworkElementFactory contentControlFactory = new FrameworkElementFactory(typeof(ContentControl));

textBlockFactory.SetBinding(ContentControl.ContentProperty, new Binding());

 

stackPanelFactory.AppendChild(textBlockFactory);

stackPanelFactory.AppendChild(contentControlFactory);

 

DataTemplate template = new DataTemplate();

 

template.VisualTree = stackPanelFactory;

 

button.ContentTemplate = template;

button.Content = new TextBlock { Text = "Test" };

 

재미있지요? 실제 Object를 생성하는 것이 아니라 FrameworkElementFactory라는 녀석을 사용하고 있습니다. 즉, Template는 이 녀석을 사용해서 자신의 ‘틀’의 모양을 구성하고 이 틀을 사용해서 자신의 틀과 일치하는 Object Tree을 마구 마구 찍어 낼 수 있는 겁니다.

 

//위의 코드에 계속해서..

template.Seal();

object obj1 = template.LoadContent();

object obj2 = template.LoadContent();

 

bool isSame = obj1 == obj2;

 

자, 간단한 퀴즈가 나갑니다. 위 코드가 실행되면 isSame이라는 변수에는 어떤 값이 들어갈까요?




















예 맞습니다. ‘false’가 들어갑니다. LoadContent()라는 메소드가 ‘너의 틀을 이용해서 Object Tree를 찍어내라!’라는 명령을 수행하기 때문입니다. 따라서 ob1과 obj2는 전혀 다른 객체입니다.

저작자 표시
신고

Code Contract -2

프로그래밍 2009.12.14 18:33 Posted by 아일레프

이전 포스팅(Beyond the Assertion, Code Contract -1)에서 Assertion Assertion으로서의 쓰임과 ‘계약’으로써 사용되는 Assertion에 대해 알아보고 그 한계에 대해 언급한적이 있습니다. 그 한계란 다음과 같았습니다.

1.     Assertion 또는 if-then throw exception을 계약의 목적으로 사용했을 때, 그 계약이 존재한다는 사실은 Runtime에 그 계약을 어겼을 때에만 확실히 알 수있다.

2.     PreCondition, PostCondition, Class Invariants 각각의 계약의 특성에 따른 고유의 특성이 필요하다.

3.     코드 중복

4.     Interface에 계약을 명시할 수 없다.

5.     상위 Type의 계약이 하위 Type에 상속되지 않을 수도 있다.

6.     컴파일시 바이너리에 포함할지 결정하는 조건을 다양하게 하고 싶다.

이번 포스팅에서는 2번에 대해서 알아보기로 합니다. 여러분은 이 포스팅으로 Code Contract를 사용해 PreCondition, PostCondition, Object Invariants를 지정할 수 있을 겁니다.

각 계약의 특성과 Code Contract기본 메소드들

n  PreCondition

메소드의 PreCondition을 만족하는지 체크하게 되고 대부분 Argument 확인을 위해 사용됩니다. 따라서 이것은 Callee를 위해 Caller가 메소드를 올바르게 사용했는지 확인하는 절차가 됩니다. 이러한 목적으로 사용되는 메소드가 Contract 클래스의 Require 메소드입니다.

public static class Contract

{

            [Conditional("CONTRACTS_FULL")]

           public static void Requires(bool condition);

           public static void Requires<TException>(bool condition) where TException : Exception;

}

Contract.Requires는 “CONTRACTS_FULL” 심볼이 Define되어있을 때만 바이너리에 추가되며, Requires<TException> Debug, Release 모두 빌드에 추가되게 됩니다. CONTRACTS_FULL심볼이 Define되어있을 때 PreCondition을 만족하지 못하면 RuntimeContractException이 발생되며 심볼과 관계없이 Requires<TException>을 만족시키지 못하면 Runtime TException을 발생시키게 됩니다.

Requires PreCondition의 특성에 따라 메소드의 시작부분 또는 변수 선언 바로 다음에만 사용할 수 있습니다. 그렇지 않으면 빌드에러가 발생합니다.

private int Factorial(int number)

{

      Contract.Requires<ArgumentException>(number >= 0);

}

 

n  PostCondition

PreCondition Caller가 메소드를 올바르게 사용했는지 확인하는 절차라면 PostCondition Caller를 위해 Callee 메소드가 계약에 맞게 수행되었는 지 확인하는 절차입니다. 이러한 목적으로 사용되는 것이 Contract클래스의 Ensures메소드입니다.

public static class Contract

{

           [Conditional("CONTRACTS_FULL")]

           public static void Ensures(bool condition);

           [Conditional("CONTRACTS_FULL")]

           public static void EnsuresOnThrow<TException>(bool condition) where TException : Exception;

}

Ensures EnsuresOnThrow모두 CONTRACTS_FULL심볼이 정의 되어있을 때만 바이너리에 포함되게 됩니다. 이것이 PreCondition PostCondition과의 중요한 차이점 입니다. Requires는 배포시에도 필요성이 있지만 – 사용자에게 잘못된 메소드 사용이라는 것을 알려주기 위해 – PostCondition의 경우는 그렇지 않기 때문입니다.

또한 PostCondition는 그 쓰임에 있어서 별도의 Helper 메소드를 필요로 합니다. 예를 들어 Factorial(number)를 호출했다고 했을 때 반환되는 값은 반드시 argument number’보다 같거나 커야합니다. 이를 어떻게 확인할 수 있을까요? 먼저 argument number가 메소드의 body에 의해서 변할 수 있으니 number를 다른 변수에 저장해 놓아야 할 것입니다. 또한 이 값과 return값을 비교해야 하기 때문에 별도의 returnResult 변수가 필요로 합니다. 코드로 다시 설명하겠습니다. 만약 이 PostCondition을 검사하지 않는 다면 Factorial메소드는 다음과 같을 것입니다.

private int Factorial(int number)

{

           Contract.Requires<ArgumentException>(number >= 0);

           if (number == 1 || number == 0) return 1;

           return number * Factorial(number - 1);

}

이 경우 PostCondition을 확인하기 위해 다음과 같은 메소드가 되어야 합니다.

private int Factorial(int number)

{

           Contract.Requires<ArgumentException>(number >= 0);

        if (number == 1 || number == 0) return 1;

        int result = number * Factorial(number - 1);

        Contract.Ensures(result >= number);

        return result;

}

그 결과 불필요한 result라는 변수가 추가되었습니다. 게다가 기존의 코드를 해치는 SideEffect가 발생했습니다. 당신이 PM이라면 당신의 사랑스러운 개발자들에게 이 일을 하라고 말할 수 있겠습니까? 어떻게 기존의 코드를 수정하지 않고 이 일을 할 수 있는 방법이 없을까요? 있습니다. -- 저는 Code Contract에서 이 기능을 가장 사랑합니다. Contract클래스의 몇가지 Helper메소드를 이용해 위와 똑같은 기능을 하는 코드를 다음과 같이 작성할 수 있습니다.

private int Factorial(int number)

{

Contract.Requires<ArgumentException>(number >= 0);

Contract.Ensures(Contract.Result<int>() > Contract.OldValue(number));

       if (number == 1 || number == 0) return 1;

       return number * Factorial(number - 1);

}

Contract.Ensures(Contract.Result<int>() > Contract.OldValue(number)); 의 의미는 이것입니다. 이 메소드의 결과값은 PreCondition state이었을 때의 number값보다 커야한다 OK, 이걸로 Contract.ResultContract.OldValue의 역할을 유추하실 수 있으실겁니다. Contract.Result<T>() return되는 결과값을 의미하는 것이고 Contract.OldValue<T>(T value) PreCondition state였을 때의 value값을 의미하는 것입니다. 그런데 아무리 그렇다고 하더라도 이 코드에는 의문점이 많이 있습니다. 아니 결과 값은 저 코드 뒤에 나오는데 어떻게 이것이 가능하단 말입니까? 여러분은 이것이 단 한가지 방법으로만 가능하다고 추측하실 수 있을 겁니다. , 맞습니다. Code Contract는 빌드 후에 IL코드를 수정하게 됩니다. 이것을 Code Contract Binary Rewriter라고 부르더군요. 이에 대해서는 나중에 자세히 알아보기로 합니다.

 

n  Class Invariants

Class Invariants 또는 Object Invariants라고 불리는 이 Condition은 모든 Public 메소드가 불린 후에 반드시 만족해야 하는 클래스의 조건입니다. 사실 이것은 모든 public 메소드에 동일한 Contract.Ensures메소드를 추가하는 것으로 이 기능을 만족할 수 있습니다. 그러나 이것 역시 귀찮고 힘든 일이지요. 이와 동일한 일을 Code Contract를 사용해 다음과 같이 할 수 있습니다.

[ContractInvariantMethod]

protected void ObjectInvariant()

{

Contract.Invariant(this.x >= 0);

Contract.Invariant(this.y >= 0);

}

위와 같은 코드를 특정 클래스에 사용하면 CONTRACT_FULL 심볼이 정의되어있을 때 Code Contract [ContractInvariantMethod] 애트리뷰트가 있는 메소드 내의 동작을 모든 public 메소드 뒤에 붙혀버립니다. 그리고 만약 이 Class Invariant를 만족하지 못하면 ContractException을 발생시킵니다.

이번 포스팅에서 가장 PreCondition, PostCondition, Class Invariant를 검사하기 위한 Code Contract의 기본 메소드들을 살펴보았습니다. 다음 포스팅에서는 Interface Contract, Contract 상속 그리고 그 이외의 주제들에 대해 말씀 드리겠습니다.

 

 

신고

Beyond the Assertion, Code Contract -1

프로그래밍 2009.12.09 19:30 Posted by 아일레프

블로그에 따로 포스팅 하지 않았지만 사실 이번에 안재우 과 함께 PDC09에 직접 참석하는 영광을 누렸습니다. 앞으로 PDC09에 참여했던 세션 중 인상 깊었던 것 들에 대해 개인적으로 리뷰할 계획입니다. 이 포스팅은 그 첫번째 입니다.

 

첫날의 마지막 세션이었던 Code Contracts and Pex : Power Charge Your Assertions and UnitTests 에서 두 명의 발표자가 “만담”형식으로 재미있게 Code Contract Pex를 함께 사용하는 방법을 소개했습니다.(약간 통통하신 분의 발음이 아주 재미있습니다.) 사실 Code Contracts Pex PDC09에 처음 소개되는 주제가 아닙니다. Pex PDC08에 발표되어 청중의 열혈한 박수를 얻어 낸 바 있으며, Code Contract또한 PDC08에 소개되었으며 Web상에서도 알려진바 있습니다.

 

하지만 이 세션을 볼 당시 전 Code Contract에 대해서는 잘 알지 못했는데 Code Contract Pex보다 더욱 쓸만할 것 같더군요. 어렵지 않게 사용할 수 있으면서도 가벼운 습관을 통해 더 나은 품질의 결과를 얻을 수 있을 거란 생각이 들었습니다. 여러분은 어떤가요? 이 글을 읽으신후 스스로 판단하시기 바랍니다.

 

 

Why the Assertion?

Assertion as Assertion

Assertion, 단언 이라고 표현되는 이 테크닉은 프로그래머가 코드의 특정 지점 내에서 프로그램 논리 흐름상의 반드시 만족해야 하는 ‘가정’을 코드에 넣는 것을 의미합니다. Introduction to Algorithm QuickSort알고리즘에 사용되는 Partition Function을 예로 들어보겠습니다.

 

Partition(A, p, r)

x ß A[r]

i ß p -1

for j ß p to r-1

           do if A[j] <= x

                     then i ß i+1

                                exchange A[i] ßà A[j]

exchange A[i] ßà A[r]

return i+1

 

책에서는 위 알고리즘을 알리는 것으로 끝나는 것이 아니라 다음과 같은 단언이 따라옵니다.

 

For 루프 안에서 다음 3가지 조건이 항상 만족한다.

1.     If p <= k <= i, then A[k] <= x

2.     If i+1 <= k <= j-1, then A[k] > x

3.     If k = r, then A[k] = x

 

3가지 expression이 바로 알고리즘 논리 상의 Assertion입니다. 만약 for 루프 내에서 1,2,3번 중 하나라도 만족하지 않는 것이 있다면 이 알고리즘은 올바르지 않은 것입니다.

다른 예로 최근 포스팅을 들 수 있겠습니다. 해당 포스팅을 보면 알고리즘 나열 도중 -**단언-으로 시작되는 줄이 보입니다. 그들이 제가 생각 했을 때 반드시 참이어야 하는 논리상의 가정입니다.(실제로는 저 단언중 몇가지가 옳지 않아 알고리즘을 수정해야 했습니다.)

프로그래머는 System.Diagnostics Debug 클래스의 Assert( bool condition)메소드를 이용해 Assertion을 코드에 삽입할 수 있습니다. Debug클래스의 Assert는 조건를 만족하지 않으면 메시지와 함께 프로그램을 종료시켜 버립니다. 이런 Assertion의 사용으로 인해 프로그래머는 자신의 알고리즘에 대해 확신을 가질 수 있고, 만약 논리상의 오류가 있을 때 그 원인을 보다 쉽게 추리할 수 있습니다.

 

Assertion as Contract.

Assertion을 프로그래머간의 ‘계약’으로 사용할 수 있습니다. Argument Check가 좋은 예입니다. 관습적으로 Public 메소드의 Argument Check Assertion이 아닌 Exception으로써 명시합니다. 예를 들어 Math 클래스의 Round메소드의 Signiture는 다음과 같습니다.

public static double Round(double value, int digits, MidpointRounding mode);

Visual Studio 상에서 위 메소드의 주석을 펼쳐보면 다음을 발견할 수 있습니다. Digits 메소드가 0보다 작거나 15보다 크면 Exception을 발생시킨다는 것입니다. 즉 이 메소드는 digits argument 0<= digits <= 15라는 전제 내에서 동작하며, 이 메소드를 이용하는 사용자가 이 전제를 지켜주기를 요구하고 있는 것입니다.

//   System.ArgumentOutOfRangeException:

//     digits is less than 0 or greater than 15.

 

이와 같이 프로그래머는 자신의 코드를 최초에 디자인 할 때 반드시 만족해야 할 “최소한의 조건”를 자신 또는 다른 사용자에게 노출 시킬 필요가 있습니다. 이것을 명시하는 것을 “계약”이라 하겠습니다. 이러한 계약의 종류로 크게 다음 3가지를 들 수 있겠습니다.

 

A.    PreCondition

특정 작업이 시작되기 전에 반드시 지켜져야 하는 전제 조건을 의미합니다. 이미 설명한 메소드의 Argument체크가 이에 해당됩니다.

메소드의 시작에 계약 코드를 삽입하며, public 메소드에는 if-then throw exception 코드가 사용되며 private 메소드에는 Assert가 사용됩니다.

 

B.     PostCondition

특정 작업이 끝난 후에 보장되어야 하는 조건을 의미합니다. 예를 들어 Arc Sine 메소드를 디자인 한다고 했을 때 결과 값이 -π/2 ≤θ≤π/2 를 만족하는 Radian값인 θ이라는 것을 보장할 수 있어야 합니다. 메소드의 마지막에 계약 코드를 사용하게 됩니다.

 

C.     Object Invariants

이것은 클래스 내의 모든 특정 작업이 종료된 후에 항상 지켜져야 하는 불변의 조건을 의미합니다. 예를 찾기 힘들어 Reference로 사용한 Programmin with Assertion의 예를 그대로 말씀드리겠습니다. 여러분이 어떤 필요에 있어서 Balanced Binary Tree 를 만들어야 한다고 가정하겠습니다. 이 때 Balanced Binary Tree는 특정 연산이 이루어지기 전이나 후에 “Balanced, Binary”라는 조건을 항상 만족해야 합니다. Tree내의 모든 Node Node자신의 자식의 수가 0보다 같거나 크고, 2보다 같거나 작다는 조건을 항상 만족해야 하며(Binary), 모든 Node Node 자신의 자식들(많아야 2)간의 Depth의 차이가 1보다 같거나 작아야한다는 사실을 만족해야 합니다.(Balanced)

모든 public 코드 뒤에 Object Invariants Assert코드가 존재하는 메소드를 호출하는 것으로 이 작업을 수행할 수 있습니다.

 

 

Break Point!

여러분은 여기까지 읽었을 때 Assertion -- if-then throw 를 제외한 순수 Assertion -- 이 프로그램을 사용하는 사람을 위한 것이 아니라 프로그램을 개발하는 사람들을 위한 것이라고 추측 할 수 있습니다. 그리고 그 추측은 완전히 옳습니다. 또한 이 추측은 또 한가지 결론으로 우리를 이끌어 가는데, 바로 Release시에는 Assert는 존재해야할 이유가 없다는 것입니다. 따라서 보통 아래와 같이 사용합니다.

#if DEBUG

AssertCode..

#end if

하지만 매번 #if DEBUG를 쓴다는 것도 힘든 일이지요. 따라서 우리를 배려한 Microsoft는 Debug.Assert 메소드가 오직 Debug시에만 컴파일되게 했습니다.

 

 

Why the Code Contract?

Code Contract as Assertion

Code Contract는 이름에서 알 수 있듯이 “계약”을 위한 필요와 목적에서 출발했지만 Assertion으로써의 기능도 할 수 있습니다. Contract클래스의 Assert, Assume 메소드가 그것입니다. 이 기능은 기존의 Debug.Assert의 기능과 완전히 동일합니다.

public static void Assert(bool condition);

public static void Assume(bool condition);

이는 물론 DEBUG시에만 컴파일 대상이됩니다. Assert Assume의 차이점은 이 후에 설명하도록 하겠습니다.

 

Code Contract as Contract

Assertion Assertion으로써의 기능만을 수행한다면 아무런 불만이 없습니다. 그러나 Assertion Contract의 기능까지 함께 할 때 몇 가지 불만이 따라오게 됩니다. 대충 생각해 보면 다음과 같습니다.

 

1.     Assertion또는 if-then throw exception를 계약의 목적으로 사용했을 때, 그 계약이 실재로 존재한다는 사실은 Runtime에 그 계약을 어겼을 때에만 확실하게 알 수 있습니다. 이것은 분명히 비극입니다.

2.     PreCondition, PostCondition, Class Invariants 각각의 계약의 특성에 따른 고유의 기능이 필요한데 기존의 Assertion, if-then throw exception은 그러한 요구를 만족시켜줄 수 없습니다.

3.     그놈의 코드 중복 때문입니다.

4.     Interface에 계약을 명시하고 싶습니다.

5.     상위 Type의 계약이 하위 Type에 ‘반드시’ 상속되었으면 좋겠습니다.

6.     컴파일시 바이너리에 포함될지 결정하는 조건을 다양하게 하고 싶습니다.

 

1번의 필요성 때문에 Code Contract 2가지 유용한 기능을 가지고 있습니다. Document Generation Static Verification 기능이 그것입니다. Documentation Generation으로 개발자들은 계약을 보기도 싫은 코드 조각과 런타임시에만 존재하는 Error 메시지가 아닌 분명한 문서로 받아 볼 수 있게 되었습니다.(문서가 더 싫은 가요? ^^) 또한 Static Verification으로 계약을 잘 지켜서 코딩 했는지 빌드 시에 Warning 메시지로 알 수 있습니다.

 

2번의 필요성 때문에 -- 2번의 필요성에 대한 자세한 설명은 다음 포스팅으로 미루겠습니다. -- Code Contract Precondition, PostCondition, Object Invariants에 특화된 별개의 메소드를 고안했습니다. Require, Ensure, Invariant가 그것입니다. 각각의 설명은 다음 포스팅에 자세히 소개하겠습니다.

 

3번은 사실 4번과 5번을 포함합니다. Assertion만으로 Object Invariants를 구현한다고 상상해보시기 바랍니다. 모든 public 메소드의 마지막에 Object Invariants를 확인하기 위한 Assert 코드를 추가해야 할 것입니다. 이러한 중복은 당연히 사라져야 합니다.

 

4번의 필요성은 상당히 중요합니다. Interface는 물론 다형성을 위한 것이지만 계약은 통일되어야만 합니다. 각각의 Interface 구현 클래스가 다른 계약을 가지는 것을 절대 허용하면 안됩니다. 따라서 Interface에 계약을 명시해야 하며, 그 계약은 모든 구현 클래스에 적용되어야만 합니다.

 

5 입니다. virtual 메소드를 가진 Super클래스가 있다고 가정합니다. Virtual 메소드안에 계약코드를 넣었는데 sub클래스에서 이것을 override해 계약 코드를 지우는 것을 허용하면 안됩니다. 때에 따라서 정해진 계약을 ‘강화’시키는 것을 막아야 할 필요성도 있을 수 있습니다.

 

 

Next

Code Contract가 등장하게 된 배경에 대해 간략히 설명했습니다. 여러분이 스스로 생각하고, 상상할 수 있는 권리를 빼앗고 싶지 않기 때문에 Code Contract가 어떻게 이 일을 해냈는지 말하는 것은 다음 포스팅으로 미룹니다.
감사합니다.
 

 

 

References

Code Contracts and Pex : Power Charge Your Assertions and UnitTests

MSDN Magazine : Code Contracts

Code Contract

Code Contract Manual

Programming With Assertions

저작자 표시
신고

Visual Studio Solution File(.sln) Parser

프로그래밍 2009.11.30 23:30 Posted by 아일레프

 

Sln 파일을 Parsing 필요가 생겼습니다. 설마 없을까 싶어 웹을 찾아 보았는데, 딸리는 검색 실력으로 찾은 것은 StackOverflow Visual Studio 2003 Sln Parse Script 정도가 전부였습니다.

 

Visual Studio 2003 Sln Parse Script C#에서 스크립트를 어떻게 이용해야 하는 모르겠고,

Stack Overflow 내용을 사용하고 싶지만 내부적으로 Visual Studio DTE 8.0 인스턴스를 생성해야 한다는 것이 문제여서.. 어쩔 수 없이 무식한 방법(정말 무식하게)으로 직접 만들었습니다.


솔루션 파일의 구조를 알기 위해 참고한 사이트는  Hack the Project and Solution Files 입니다.

 
첨부된 프로젝트는 해당 기능을 하는 Parser입니다.

Solution.Parse(solutionFilePath)로 사용가능합니다. 





저작자 표시
신고

WPF Dialog 컨트롤들

프로그래밍 2009.11.11 20:00 Posted by 아일레프

 

최근 FolderBrowserDialog 필요한 일이 생겼는데, 아쉽게도 WPF에는 존재하지 않았다. 다행히 검색해보니 ookii.org에서 Ookii.Dialog 이름으로 여러 Dialog들을 공개하고 있었다.

 

 Ookii.Dialogs 사이트를 통해 확인 있다.

저작자 표시
신고
TAG Dialog, WPF

한글 오토마타

프로그래밍 2009.11.04 23:30 Posted by 아일레프

최근 한/영 자동 고침을 지원하는 TextBox를 만들어야 했다. Low level api를 사용하지 않고 단순한 문자열 변환만을 이용해 한/영 고침을 지원하기 위해 어쩔 수 없이 한글 Automata가 필요하게 되었다. 예를 들어, tkfkd 이라고 사용자가 입력했을 때 "사랑"이라고 출력이 나와야 하는데 이는 다음과 같은 동작을 통해 이루어 질 수 있다.

tkfkd -> ㅅㅏㄹㅏㅇ -> 사랑

이 때 ㅅㅏㄹㅏㅇ -> 사랑 으로 변환하기 위해 사용되어야 하는 것이 한글 Automata이다. 웹검색을 통해 알아봤는데 두벌식 한글 입력을 위해서 imhangul에서 사용한 Automata 사이트에 있는 아래 Automata가 가장 괜찮아 보였다.

 

하지만 위 Automata는 키보드의 즉각적인 입력을 바로 한글로 나타내는 것에 목적이 있지만 내가 만들어야 하는 것은 처음과 끝이 정해진 한글 호환 자모로만 이루어진 문자열을 한글로 변경하는 것이기에 위 사이트의 Automata보다 간편해질 수 있는 여지가 있다. 또한 위 Automata는 다음 두가지 측면에서 나와 같은 초보 프로그래머가 구현하기 까다로운 측면이 있었다.

  1. 특정 State는 이전 State의 정보를 필요로 한다.

    위 사이트를 가보면 CxSy이라는 것은 x State까지의 입력을 Commit하고, y State로 이동하는 것을 의미한다고 되어있다. 위 그림에서 5번 State를 보자 5번 state에서 모음입력이 오면 C3S3이므로 3번 State까지의 입력을 Commit하고 3번 State로 이동하게 된다. 이는 9번 State도 마찬가지이다. 자, 어떻게 state가 이전 state의 정보를 가지고 있게 할 것인가? 내 머리로 깔끔한 해결책을 찾기에는 무리였다.

  2. 각 State의 동작을 정하는 분기 조건이 일치하지 않는다.

    위 그림을 보면 특정 State는 분기 조건이 자음입력, 모음 입력 밖에 없는데, 어떤 State는 분기 조건이 v, vc, l, t 4가지 이다. 분기조건이 C, vc, v 3가지인 State도 있다. 이것은 내가 프로그램을 짜기 곤란하게 만들었는데 왜냐면 나는 각 State를 특정 분기가 일어났을 때의 행동을 정의하는 특정 Interface를 구현하게 해서 최종적으로 StatePattern을 사용하고 싶었기 때문이다.

    그래서 새로운 한글 Automata를 구현하기로 결정했다. 그 녀석은 다음과 같다.

    쩝, Automata라기 보다는 State로 표현된 흐름도라고 보는 게 좋을 것 같다. 워낙 허접해서..

    State를 표현하는 원에는 숫자가 있는데 이는 각 State가 관심이 있는 버퍼의 index를 말한다. 그리고 분기조건은 버퍼[관심있는 index]가 자음인지, 모음인지, 글자 end인지 판단하는 것이다.

    또한 Commit, Canbe중C, Canbe자C, Combine등은 함수를 칭한다. 다음과 같은 동작을 한다.

  3. Commit(bu[0~n]) : 지정된 버퍼의 한글 호환 자모를 꺼낸 후 조합해 출력에 더한다. 예를 들어서 Commit(bu0,1)은 버퍼 0번 1번에 있는 한글 호환 자모를 조합해 출력에 더한다는 것이다. Commit(ㅁㅏㅁ)이면 '맘'을 반환한다.
  4. Canbe중C(모음1, 모음2) : 모음 1, 모음 2가 복모음으로 조합될 수 있는 지 검사한다. Canbe중C(ㅗ, ㅏ)이면 true를 반환하게 된다.
  5. Canbe자C(자음1, 자음2) : 자음 1, 자음 2가 복자음으로 조합될 수 있는 지 검사한다. Canbe자C(ㄹ, ㅁ)이면 true를 반환하게 된다.
  6. Combine(문자, 문자) 는 복모음, 복자음으로 조합 가능한 두 한글 호환 자모를 입력 받아 조합한 뒤 반환한다. 예를 들어 Combine(ㄹ, ㅁ)은 ㄻ을 반환한다.

    버퍼가 [ㅅㅏㄹㅏㅇ]이었을 때 동작과정을 보면 다음과 같다.

  7. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  8. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  9. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  10. 3자 State (관심 index : 3) - 버퍼[3]이 모음이므로 Commit(bu0,1)를 수행하고 0 State로 이동한다.

    버퍼 - [ㄹㅏㅇ], 출력 - 사

  11. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  12. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  13. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  14. 3자 State (관심 index : 3) - 버퍼[3]이 BufferEnd이므로 Commit(bu0,1,2)를 수행하고 0State로 이동한다.

    버퍼 - [], 출력 - 사랑

  15. 0State (관심 index : 0) - Buffer End이므로 Automata를 종료한다.

      

    버퍼가 [ㅁㅏㄹㅁㅇㅡㅁ] 이었을 때의 동작과정은 다음과 같다.

  16. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  17. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  18. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  19. 3자 State (관심 index : 3) - 버퍼[3]이 자음이므로 4자 State로 이동
  20. 4자 State (관심 index : 4) - 버퍼[4]이 자음이므로 CanBe자C(bu2,3)인지 검사, CanBe자C(ㄹ,ㅁ)이므로 true, 따라서 버퍼 2,3을 합한다. 이 때 버퍼는 [ㅁㅏㄻㅇㅡㅁ] 가 된다. 그리고 Commit(bu0,1,2)를 수행한 후 State0으로 이동한다.

    버퍼 - [ㅇㅡㅁ] 출력 - 맑

  21. 0 State (관심 index : 0) - 버퍼[0]이 자음이므로 1자 State로 이동
  22. 1자 State (관심 index : 1) - 버퍼[1]이 모음이므로 2모 State로 이동
  23. 2모 State (관심 index : 2) - 버퍼[2]이 자음이므로 3자 State로 이동
  24. 3자 State (관심 index : 3) - 버퍼[3]이 BufferEnd이므로 Commit(bu0,1,2)를 수행하고 0State로 이동한다.

       

    버퍼 - [], 출력 - 맑음

      

    이제는 구현만 남았다. Commit, Canbe중C, Canbe자C, Combine각각의 함수를 만들고 각 State를 다음의 Interface를 구현하게 한다.

    public interface IHangulAutomataState

    {

    HangulAutomataContext HangulContext { get; set; }

    IHangulAutomataState NextState { get; }

    int InteresteBufferIndex { get; }

    void DoStateActionWhenConsonantComes();

    void DoStateActionWhenVowelComes();

    void DoStateActionWhenBufferEndComes();

    }

    public class HangulAutomataContext

    {

    public List<char> Buffer { get; set; }

    public string OutString { get; set; }

    }

    그리고 HangulAutomata는 다음과 같이 구현하면 되겠다.

       

    public class HangulAutomata

    {

    public static string ConvertToCompleteHangul(string sentence)

    {

    HangulAutomataContext context = new HangulAutomataContext { Buffer = sentence.ToCharArray().ToList(), OutString = string.Empty };

    IHangulAutomataState automataState = HangulAutomataFactory.GetAutomataState(0, JamoType.None, context);

    while (true)

    {

    if (automataState.InteresteBufferIndex == context.Buffer.Count)

    {

    automataState.DoStateActionWhenBufferEndComes();

    if (context.Buffer.Count == 0)

    {

    return context.OutString;

    }

    }

    else

    {

    switch (GetJamoType(automataState.HangulContext.Buffer[automataState.InterestedBufferIndex]))

    {

    case JamoType.Consonant:

    automataState.DoStateActionWhenConsonantComes();

    break;

    case JamoType.Vowel:

    automataState.DoStateActionWhenVowelComes();

    break;

    }

    }

    automataState = automataState.NextState;

    }

    }

    }

       

       

신고

DependencyProperty Value Inheritance

프로그래밍 2009.08.26 12:00 Posted by 아일레프

의존 프로퍼티에 대해서 하나 더 포스팅 합니다. 사실 이 포스팅을 하면서 두려운 마음이 많이 드는데, 제가 WPF를 만든 사람도 아닐 뿐 더러 아래 내용은 어디까지나 저 개인의 추측과 실험으로 나온 결과이기 때문입니다. 무책임 한 말일 수 있지만, 절대 아래 내용을 완전히 신뢰하지 마시기 바랍니다. 옳고 그름을 스스로 판단해주시고, 혹시 틀린 부분이 있나, 더 개선될 수 있는 부분이 있다면 댓글 남겨주시면 감사드리겠습니다. 그리고 아래의 내용은 결론을 얻기 까지 제 사고의 흐름입니다. 너무 길어 보기 싫으신 분은 가장 아래의 결론 부분만 읽어 주세요.

일단 의존 프로퍼티에 대한 정의를 한번 하고 넘어가겠습니다. 제가 가장 좋아하는 의존 프로퍼티에 대한 정의는 페졸드 아저씨의 정의입니다.

"It is a property that depend on number of other properties and outside influences"

다른 환경에 의해 영향을 받는(또는 의존하는) Property라는 것이죠. 이런 의존 프로퍼티에 대한 개념을 잡아 주기 위해 대부분의 책들이 Property Inheritance를 예로 듭니다. 오늘의 이슈는 바로 이녀석입니다.

 위와 같이 VisualTree가 구성되어있을 때 Window의 FontSize를 30으로 만들면 Button의 FontSize도 자동으로 30으로 설정되죠. 즉 Button의 FontSize라는 Property는 Button의 부모인 Window의 FontSize Property에 영향을 받고 있습니다. 그런데 이게 어떻게 가능할까요?

   

Property Inheritance를 위한 부모와 자식간의 관계는 어떻게 설정되는 것일까?

위와 같은 그림에서 Window와 Button은 직접 부모, 자식 관계로 이어져 있을 까요? 답은 그럴 수도 있고, 아닐 수도 있습니다. 위 Window와 Button은 VisualTree내에서 직접적인 부모, 자식 관계가 아닙니다. 가장 최소화된 VisualTree라 하더라도 Window->ContentPresenter->Button으로서 직접적인 부모, 자식으로 이어지지 않습니다. 하지만 Logical Tree내에서는 직접적인 부모, 자식간의 관계로 이루어져 있습니다. 조금 만 더 깊게 들어가보면 VisualTree는 Window의 Template과 ContentTemplate에 의해 영향을 받게 된다는 것을 알 수 있습니다. 그렇다면 LogicalTree는 어떻게 구성되는 걸까요? 질문 1. LogicalTree는 단지 VisualTree를 단순화한 Tree일까요?

여기서 알아야할 중요한 사실이 있는데, 특정 Control A의 Logical Tree내의 자손이 Control A의 VisualTree내의 자손이 아닐 수도 있다는 사실입니다. 이것을 가장 크게 보여주는 예가 바로 Grid입니다. Grid의 LogicalTree내에는 Grid의 Child로 지정된 UIElement들 뿐 아니라 Visual객체가 아닌 RowDefinition, ColumnDefinition도 존재하고 있습니다. 질문 1의 해답. LogicalTree는 VisualTree를 단순화한 Tree가 아니다. LogicalTree와 VisualTree를 별개로 보는 것이 오히려 타당하다.

   

질문 2. VisualTree는 Template에 의해 결정된다고 말한 바 있습니다. 그렇다면 LogicalTree는 어떻게 구성되는 걸까요?

질문 3. Property Inheritance의 혜택은 Visual Tree를 통해서 얻어 질 수 있을까요, 아니면 Logical Tree로 얻을 수 있을 까요? 그게 아니면 둘 다일까요?

위 질문들의 해답은 Of logical and visual trees in WPF를 보면 얻을 수 있습니다. 결론만 말하겠습니다.

질문 2의 해답 : LogicalTree는 Template이 아닌 명시적으로 구현해 주어야 합니다. FrameworkElement를 상속받은 객체는 AddLogicalChildren이라는 메소드를 가지는데 이 메소드를 통해서 특정 객체를 자신의 LogicalChildren으로 넣을 수 있고, LogicalChild의 모든 이점을 얻을 수 있습니다. 그런데 이렇게 AddLogicalChildren을 이용해 넣은 후에도 LogicalTreeHelper를 통해 검색해도 해당 객체가 보이지 않을 수 있는데, 이것은 명시적으로 LogicalChildren을 override해주지 않았기 때문입니다.

  1. AddLogicalChildren을 통해 LogicalChild에 추가하고 싶은 객체를 추가한다.(필수)
  2. LogicalChildren을 override해 LogicalTreeHelper를 통해 해당 객체가 검색될 수 있도록 한다.(optional)

질문 3의 해답 : VisualTree, LogicalTree 모든 Tree를 통해서 Property Inheritance의 혜택을 얻을 수 있습니다. 단 해당 member가 LogicalTree에도 속해있고, VisualTree에도 속해있을 경우 LogicalTree의 Parent의 Property가 Child에게 Inherit됩니다.

이는 Of logical and visual trees in WPF에도 나와 있는 간단한 실험을 통해 확인 할 수 있습니다.

위와 같은 상황에서 ContentControl의 Content인 TextBlock은 ContentControl의 Logical Child이며, Label의 Visual Child이기도 합니다. 따라서 TextBlock의 FontSize는 40이 됩니다. 만약 ContentControl의 FontSize를 40으로 설정하지 않았다면 TextBlock의 FontSize는 20이 되겠죠.

   

그럼 실제 Property Inheritance의 구현을 유추해봅시다. (아래의 내용은 실제 WPF가 의존 Property를 설정하는 방식이 절대 아닙니다.)

Public double FontSize
{
  Get{ return (doble)GetValue(FontSizeProperty);}
  Set{ SetValue(FontSizeProperty, value);}
}

이는 곧 위 코드의 SetValue의 구현을 유추해본다는 의미입니다.(오직 PropertyInheritance만 지원하는) 다음과 같은 논리라면 가능할 것 같습니다.

Private void SetValue(DependencyProperty dependencyProperty, value)
{
    1.
value를 "dependencyProperty value Dictionary에 넣는다." (무슨 말인지 모르시는 분은 이전 포스팅 왜 Dependency Property는 public static readonly로 선언하는가?를 읽어 보시기 바랍니다.) 

    2.
dependencyProperty가 Inheritance를 허용하지 않는다면 메소드를 빠져나온다. :: 이는 해당 DependencyProperty의 metadata를 통해 알 수 있습니다.

    3.
VisualTree상의 자기 자식들이 해당 dependencyProperty를 소유하고 있는가? 
       
만약 해당 dependencyProperty를 소유하고 있으며, 해당 Property의 value가 명시적으로 설정되지 않았다면(Property Inheritance보다 우선순위가 높은 환경에 의해 설정되지 않았다면) 자식의 Property값을 변경한다. 

    4.
LogicalTree상의 자기 자식들에 대해 3번과 동일한 논리를 수행한다.
}

위 작업으로 앞서 알아본 요구사항을 만족할 수 있습니다. 여기서 중요한 것은 바로 3번과 4번의 다음 질문입니다. "자기 자식들이 해당 dependencyProperty를 소유하고 있는가?" 지금 당신이 생각하는 그것이 맞습니다. 예, 바로 Owner의 개념입니다. DependencyProperty 그 자체는 value와 관계 없이 metadata로 이해되거나 value와 metadata를 얻을 수 있는 특정 key로 이해되어야 한다고 이전 포스팅에 말한 바 있습니다. 그렇기에 Owner의 개념이 필요합니다. 우리가 DependencyProperty를 register메소드를 통해 "등록"할때 지정하는 owner가 바로 그것입니다.

위 그림을 다시 보겠습니다. Window의 FontSize가 30으로 변경되었습니다. 이 때 FontSize에 대응되는 DependencyProperty는 곧 Control.FontSizeProperty이며 owner가 Control입니다. Button은 Control을 상속받았기에 Button도 곧 FontSizeProperty의 owner입니다. 따라서 Property Inheritance가 성립되는 것입니다.

    

자 그러면 다음과 같은 것을 해봅시다.

Control이 아닌 FrameworkElement라를 상속 받는 녀석을 만들고, 이 녀석이 Window의 FontSize Property를 상속받게 하는 것입니다.

Window의 FontSize가 상속된다면 testObj의 FontSize도 30이 되겠죠. 그런데 PropertyInheritanceTest는 FontSizeProperty의 Owner가 아닙니다. 어떻게 하면 될까요? Bravo! 맞습니다. 당신은 멋쟁이! PropertyInheritanceTest를 아래와 같이 변경하면 됩니다.

 

위와 같이 한 후 breakPoint를 사용해 PropertyInheritanceTest의