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

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

저작자 표시
신고

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 에서 확인 할 수있다.


저작자 표시
신고

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는 전혀 다른 객체입니다.

저작자 표시
신고

WPF Dialog 컨트롤들

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

 

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

 

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

저작자 표시
신고
TAG Dialog, WPF

Layout System in WPF : 프롤로그

프로그래밍 2009.05.21 20:49 Posted by 아일레프

모든 일은 그 메일로 부터 시작되었다.

- "Grid 안의 모든 Element 들의 간격을 일정하게 띄울 수 없을 까요? Html의 CellPadding 같은 역할을 할 수 있는 어떤 방법이 있을 것 같은데요" -

   일단 Grid 내의 모든 Element의 간격을 띄울 수 있는 방법은 있다. Grid내에 선언된 모든 Element의 Margin값을 1~2로 설정하는 것이다. ^^;; 그러나 이것은 중복된 코드를 발생시키게 되고 당연히 솔루션이 될 수 없다. 이 문제로 몇 개의 메일이 왔다 갔고 그나마 중복을 줄일 수 있는 방법이 Grid내에 사용되는 모든 Control들의 Default Style을 지정해 Margin을 1로 주는 것이라는 것을 서로 확인 했다.

 물론 가장 쉬운 방법은 코드상에서 해당 Grid에 Loaded 이벤트를 걸어 Grid Children들의 Margin에 특정 값들을 더해주는 것이다. ^^;; …(죄송합니다.)

 몇 번 이 문제에 대해 묵상을 하니 그나마 가장 좋은 방법은 역시 Attached Property 라는 생각이 들었다. 다음과 같이 구현 될 수 있겠다.

 

일단 FrameworkPropertyMetadataOptions.Inherits를 사용하고 있다. 따라서 이 Attached Property는 상속되게 된다.

위 경우 Grid뿐 아니라 RowDefinition과 Button에도 CellPadding Attached Property가 20으로 지정되게 된다. 따라서 2개의 버튼과 RowDefinition 객체가 로드 될 때 저 이벤트가 호출되게 되고 Margin값에 CellPadding값을 더해주게 된다.

왜 굳이 이렇게 했을까? Inherits를 사용하지 않으면 sender를 Grid로 하는 이벤트만 호출 될 테고 그 때 Grid의 모든 Children들의 Margin값에 CellPadding값을 더해주면 간편할텐데 말이다.

위와 같이 하면 더 간편할 것 같은데 이렇게 하지 않은 것은 이유가 있다. 왜 안되는지 잠깐 생각해 보자. 이건 퀴즈 ^^;;;

위 코드에는 또 이상한 점이 있다. SetCellPadding 메소드를 불러 다시 CellPadding 첨부 프로퍼티의 값을 0으로 만드는 것인데, 이는 이 첨부 프로퍼티의 값이 계속 타고 들어가 Grid 내에 또 다른 Grid가 있다면 이 Grid에도 영향을 주기 때문에 추가된 것이다. 물론 위 XAML에서와 같은 경우라면 명시적으로 해당 Grid에서 CellPadding값을 0으로 주면 된다. 그러나 만약 특정 Element의 ControlTemplate내에 Grid가 있다면 문제가 복잡해 질 수 때문에 이를 방지하고자 꼼수지만 0으로 만들었다.

어찌 되었건 위와 같이 잘 적용 되었다. 하지만 이 솔루션에는 단점이 있다. 일단 해당 Attached Property가 Grid가 아닌 다른 Pannel이나 Control에도 정의 될 수 있다는 것이 마음에 안들고 Attached Property가 Tree를 계속 타고 내려가 실제로는 아무 일도 하지 않는 저 메소드를 계속 실행한다는 것이 또 마음에 안든다.

결국 가장 좋은 해답은 Grid를 상속받아 재 정의해 CellPadding 프로퍼티를 추가시키는 것이라는 생각이 들었다. 그래서 과감하게 Grid를 상속받은 GridEx란 클래스를 정의하고 MeasurreOverride와 ArrangeOverride 메소드를 재 정의 했는데... 아… 이 때는 알지 못했다. Grid가 모든 Panel중에서 가장 많은 일을 하고 있는 복잡한 Panel이라는 것을… 결국 멈출 수 없는 수렁으로 빠져 들어가게 되는데…

   

   

   

… 이 시리즈가 계속 될 수 있을까요? ^^;;;

신고

WPF Command-Part4 : DelegateCommand

프로그래밍 2009.02.26 22:24 Posted by 아일레프

(미루고 미루다 이제야 마지막 포스팅이네요. ㅜ.ㅜ)

지난 포스팅까지 RoutedCommand에 대해서 살펴보았습니다. RoutedCommand라는 이름이 붙은 이유는 Command Target을 찾기 위해RoutedEvent를 사용하기 때문이라는 것도 알 수 있었죠. 그런데 개발자들은 다른 유형의 Command에 대한 필요성을 느끼게 되었습니다.

적절한 UI를 구성하기 위해 개발자들이 먼저 생각하는 것은 바로 View와 논리의 분리일 것입니다. 현재 UI Design Pattern으로 널리 사용되는 MVC, MVP, MVVM의 경우도 View와 Model, Logic의 결합도를 줄임으로써 확장성을 늘리고 테스트 하기 쉬운 구조를 만들어 낸 바 있습니다. 그런데 RoutedCommand의 경우는 RoutedCommand가 CommandTarget을 View의 Visual Tree내에서 찾기 때문에 View와 Logic을 완전히 분리하기 어렵습니다. 때문에 여러 개발자들은 다른 Command에 대한 필요성을 느끼게 되었고 그 결과 DelegateCommand라는 녀석을 만들어 냈습니다. 그리고 이 DelegateCommand는 Composite Application Guidance for WPF에 포함되게 됩니다. MSDN Magazine에 소개된 DelegateCommand의 구현 코드는 다음과 같습니다.

public class StringDelegateCommand : ICommand {
  Action<string> m_ExecuteTargets = delegate { };
  Func<bool> m_CanExecuteTargets = delegate { return false; };
  bool m_Enabled = false;
  public bool CanExecute(object parameter) {
    Delegate[] targets = m_CanExecuteTargets.GetInvocationList();
    foreach (Func<bool> target in targets) {
      m_Enabled = false;
      bool localenable = target.Invoke();
      if (localenable) {
        m_Enabled = true;
        break;
      }
    }

    return m_Enabled;
  }
  public void Execute(object parameter) {
    if (m_Enabled)
      m_ExecuteTargets(parameter != null ? parameter.ToString() : null);
  }
  public event EventHandler CanExecuteChanged = delegate { };
  ...
}
간단하지요? 위 녀석은 Command Parameter로 string 객체를 전달하기 때문에 StringDelegateCommand라는 이름이 붙었습니다. 좀 더 일반적인 녀석을 만들어볼까요? 다음과 같이 만들 수 있겠네요.
public class DelegateCommand<T> : ICommand
{
        Action<T> executeTargets = delegate { };
        Func<bool> canExecuteTargets = delegate { return false; };
        bool m_Enabled = false;
        public bool CanExecute(object parameter)
        {
            Delegate[] targets = canExecuteTargets.GetInvocationList();
            foreach (Func<bool> target in targets)
            {
                m_Enabled = false;
                bool localenable = target.Invoke();
                if (localenable)
                {
                    m_Enabled = true;
                    break;
                }
            }
            return m_Enabled;
        }
        public void Execute(object parameter)
        {
            if (CanExecute(parameter) == true)
                executeTargets(parameter != null ? (T)parameter : default(T));
        }
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public event Action<T> ExecuteTargets
        {
            add
            {
                executeTargets += value;
            }
            remove
            {
                executeTargets -= value;
            }
        }
        public event Func<bool> CanExecuteTargets
        {
            add
            {
                canExecuteTargets += value;
            }
            remove
            {
                canExecuteTargets -= value;
            }
        }
    }
위 녀석은 Generic으로 구현된 DelegateCommand입니다. Command Parameter로 사용하기 위한 Type을 T로 지정해 사용하면 됩니다. 그리고 이 경우 ExecuteTargets에 Handler를 등록함으로써 하나의 Command이 실행되었을 때 여러 개의 Command Target이 실행되게 할 수 있습니다.

자, 다음과 같은 질문을 해봅시다. 왜 굳이 Command를 사용하는 것이 좋을까요? 만약 특정 버튼을 눌렀을 때 "Save"란 일을 하고 싶다면 이것은 충분히 이벤트 만으로 가능해 보이는데 말입니다. 다음과 같이 말이죠. 

<Button Click="Button_Click" Content="Save"/>
private void Button_Click(object sender, RoutedEventArgs e)
{

    Save();
}


위와 똑 같은 일을 하는 녀석을 Command로 만들어 보면 다음과 같을 것입니다.

<Button Command="{Binding SaveCommand}" Content="Save"/>
public Window1()
{
      InitializeComponent();
     
this.DataContext = this;
}
public ICommand SaveCommand
{
      
getreturn saveCommand; }
} 
어떤 차이가 있는지 보이십니까? 언뜻 보면 이벤트나 Command나 거기서 거기 같아 보입니다. 그러나 중요한 사실이 있습니다. Application을 구현할 때는 Business Logic을 변경할 때 보다 View를 변경하는 일이 더욱 많다는 사실입니다. Event로 Save Button을 만들었다고 합시다. 만 약 그 후에 고객이 Save Menu를 만들어 달라고 요청했다고 가정해 보겠습니다. 이 경우 event를 사용했다면 각 메뉴항목마다 이벤트를 또 걸어주어야 합니다. 만약 Command를 사용했다면? 단순히 xaml만을 교체해 Command를 Binding하는 것으로 끝나게 될 것입니다. Command로 느슨하게 결합되어 있기 때문입니다.

또한 제가 좋아하는 Command의 강력한 기능은 CanExecute입니다. 위 Save버튼이 활성화 되기 위한 조건을 CanExecute에 만들어 놓으면 모든 경우에 대해 버튼의 활성화 여부에 대해 걱정할 필요가 없습니다. 만약 Login 창을 만든다고 가정해 볼까요? UserID와 Password를 입력할 수 있는 TextBox가 필요하겠네요. 그리고 Login버튼이 있을 것입니다. 이 Login버튼이 활성화 될 수 있는 조건은 UserId와 Password TextBox에 적당한 Text가 쓰여졌을 때일 것입니다. 만약 Command를 사용하지 않는다면? 우리는 사용자가 새로운 문자를 UserId, Password에 입력할 때 마다 Login버튼의 활성화 여부를 확인해야 할 것입니다. 굉장히 우울한 사실이죠.

Command는 또한 Model, View, Logic을 분리하기 위해 많이 사용되는 MVP, MVVM Pattern에도 사용됩니다. MVVM패턴의 경우 직접 UIElement를 만드는 것이 아니라 보통 Template를 활용하는데 이 때 Template내에서 event를 사용하는 것이 아니라 Command를 바인딩 해주는 것이죠. 이 MVVM Pattern을 사용하면 Compile을 다시 하지 않고도 View를 교체하는 것이 가능해집니다. Event를 사용했을 때는 이와 같은 효과를 얻을 수 없습니다. Command Invoker와 Command Target이 Binding을 통해 느슨하게 결합되었기에 얻을 수 있는 이점이라 할 수 있습니다. 간단히 Person정보를 저장하는 녀석을 만들어 보겠습니다. 먼저 Model입니다.

public class PersonModel : INotifyPropertyChanged
{
        private string name;
        private string address;
        public string Name
        {
            set 
            {
                name = value
                if(PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Name"));
                }
            }
            get 
            {
                return name; 
            }
        }
        public string Address
        {
            set
            {
                address = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Address"));
                }
            }
            get
            {
                return address;
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void Clear()
        {
            Name= "";
            this.Address= "";
        }
    }

ViewModel은 다음과 같을 것입니다.
public class PersonViewModel
{
        PersonModel personModel = new PersonModel();
        public PersonModel PersonModel
        {
            get { return personModel; }
        }
        DelegateCommand<string> saveCommand; 
        public DelegateCommand<string> SaveCommand
        {
            get { return saveCommand; }
        } 
       
public PersonViewModel()
       
{
            saveCommand = new DelegateCommand<string>();
            saveCommand.CanExecuteTargets += () => !string.IsNullOrEmpty(personModel.Name) && !string.IsNullOrEmpty(personModel.Address);
            saveCommand.ExecuteTargets += s => this.Save(s);
        }
        private void Save(string parameter)
        {
            //Save Operation 

            //Clear PersonModel
           PersonModel.Clear();
        }
    }

View를 만들어 볼까요? 귀찮아서 그냥 윈도우로 하겠습니다.
<Window.Resources>
        <DataTemplate  DataType="{x:Type local:PersonViewModel}" >
            <StackPanel>
                <TextBox Text="{Binding PersonModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                <TextBox  Text="{Binding PersonModel.Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                <Button Content="Save" Command="{Binding SaveCommand}"/>
            </StackPanel>
        </DataTemplate>
        <local:PersonViewModel x:Key="PersonViewModel"/>
    </Window.Resources>
    <Window.Content>
        <StaticResource ResourceKey="PersonViewModel"/>
    </Window.Content>

위와 같이 Template를 사용하기 싫으신 분도 있을 텐데 그러면 다음과 같이 만들어 주면 됩니다. Template를 써야 MVVM Pattern이 되는 것은 아니니까요.  

    <
Window.Resources>
        <local:PersonViewModel x:Key="PersonViewModel"/>
    </Window.Resources>
    <Window.Content>
        <StackPanel DataContext="{StaticResource PersonViewModel}">
            <TextBox Text="{Binding PersonModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBox  Text="{Binding PersonModel.Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Save" Command="{Binding SaveCommand}"/>
        </StackPanel>
    </Window.Content>
엄청나게 간단한 예제네요^^;; 쉽지요?

Command에 대한 Posting을 이제 끝내려고 합니다. . 생각했던 것 외로 제가 확실하게 몰랐던 부분이 있어 시간이 더 걸렸던 것 같습니다. 그러나 Command에 대한 논의는 이것으로 끝나지 않을 것 같습니다. 사실, 정확하게 WPF가 RoutedCommand를 어떻게 처리하고 CommandManager의 역할은 아직 잘 모르겠습니다. 나중에 살펴보고 제가 명확하게 알았다고 인지했을 때 여러분께 공유하도록 하겠습니다.

사족 :
사실, 예제로 Login을 들려고 했었는데 심각한 문제가 있었습니다. Password를 넣기 위해 사용되는 PasswordBox의 Password Property가 DependencyProperty가 아니라 Binding을 지원하지 않는 문제였지요. 잠시 패닉상태였습니다. ㅋ 이 부분에 대해 google에 Search를 해보니 멋진 글이 있어 소개하려 합니다. Attached Property를 이용해 PasswordBox의 Password를 Binding하는 예가 담겨있습니다. 사람들, 참 머리 좋아요~ WPF PasswordBox and Data binding
그리고 MSDN Magazine의 WPF Apps With The Model-View-ViewModel Design Pattern입니다.
마지막으로 The Build Your Own CAB Series Table of Contents 도 실로 멋지죠.

신고

WPF Command-Part3 : RoutedCommand2

프로그래밍 2009.02.02 20:01 Posted by 아일레프

이번 포스팅도 RoutedCommand가 Invoked되었을 때 RotedCommand 객체가 Command Target을 찾는 과정을 설명하도록 하겠습니다.

Command Target을 찾는 다는 것은 곧 ExecuteEvent Handler를 찾는다는 것을 의미하며, 일반적으로 Command가 Invoke되었을 때 ExecuteEvent 이벤트는 VisualTree내에서 Command를 Invoke를 시킨 Object부터 Tree의 루트까지 타고 올라가게 됩니다.

위와 같은 VisualTree에서 Button1이 특정 RoutedCommand를 Invoke를 했다면 ExecuteEvent는 Button1 -> Grid -> Window로 타고 올라가게 되는 것이고 이는 Button2에서도 마찬가지 입니다. 이것이 가장 일반적인 RouteCommand의 라우팅 메커니즘입니다. 하지만 이 Routing 방법으로는 충분하지 않은 경우가 있습니다. 다음과 같은 경우를 생각해봅시다.

 
위 프로그램은 아래와 같은 VisualTree로 구성 되어있습니다.

코드는 아래와 같습니다.

<Window x:Class="CommandTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loacl="clr-namespace:CommandTest"
Title="Window1" Height="300" Width="300">
<Grid
    <Grid.RowDefinitions
        <RowDefinition Height="Auto"/> 
        <RowDefinition/> 
    </Grid.RowDefinitions
    <Menu
       
<MenuItem Header="Cut" Command="ApplicationCommands.Cut"/> 
    </Menu
    <TextBox Height="20" Grid.Row="1"/> 
</Grid>
</Window>

TextBox에 글자를 쓰고 선택한 후 MenuItem의 "Cut"을 누르면 선택된 글자가 잘려지는 것을 확인할 수 있습니다. 그런데 두 가지 이상한 점이 있습니다. 일단 ExecuteEventHandler를 등록하는 CommandBinding객체가 정의 되어있지 않네요. 일반적으로 사용자는 Cut, Copy, Paste Command에 CommandBinding으로 ExecuteEventHandler를 등록할 필요가 없는데요, 이는 TextBox가 기본적으로 Cut, Copy, Paste Command에 대한 CommandBinding객체를 내장하고 있기 때문입니다. 따라서 이 질문은 해결 되었네요. 두 번째로 Cut이벤트가 발생했을 때 ApplicationCommands.Cut Command객체는 어떻게 TextBox의 ExecuteEventHandler를 찾아갈 수 있었을 까요? Visual Tree를 따라가 보면 다음과 같이 Routing이 진행될 것입니다.

MenuItem->Menu->Grid->Window

Tree의 Root에 이를 때 까지 ApplicationCommands.Cut에 바인딩 되어있는 Command Target을 찾지 못했으므로 아무 일도 벌어지지 않아야 할 것 같은데 분명히 "Cut" Command가 잘 동작하고 있으니 이상한 일입니다.
이는 RoutedCommand의 Routing의 두 번째 원칙 때문입니다. RoutedCommand는 CommandInvoker 부터 Tree의 Root에 이를 때 까지 CommandBinding된 객체를 찾지 못하면 CommandInvoker와 Tree Root가 동일한 Focus Scope에 있는지 확인 합니다. 만약 CommandInvoker와 Tree의 Root가 동일한 Focus Scope에 있지 않다면 RoutedCommand는 Tree Root의 Logical Focus를 가지고 있는 컨트롤을 기점으로 해서 다시 Routing을 시작합니다. 즉 다음과 같이 Routing이 되는 것입니다.

첫 번째 Routing : MenuItem->Menu->Grid->Window ---- CommandTarget 찾기 실패 
 
MenuItem과 Window가 동일한 Focus Scope에 있지 않으므로 Window의 Local Focus를 가지는 Control을 기점으로 다시 Routing시작

두 번째 Routing: TextBox -> CommandTarget찾기 성공

위와 같은 방법으로 RoutedCommand는 CommandTarget을 찾을 수 있었던 겁니다. 위의 일이 가능하게 하기 위해서는 MenuItem과 Window가 다른 FocusScope내에 있어야 하는데 WPF에서 Menu와 ToolBar와 같은 Control은 기본적으로 FocusScope을 가지고 있기 때문입니다.

이번에는 다음과 같은 시나리오를 생각해 보겠습니다.
 

<Window x:Class="CommandHandler.Window1"

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

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

    Title="Window1" Height="300" Width="300" >

   

    <Grid>

        <StackPanel>

            <Button Command="ApplicationCommands.Cut">Cut</Button>

            <TextBox Name="textBox"/>

        </StackPanel>       

    </Grid>

</Window>

위 프로그램에서 TextBox에 글자를 쓰고 선택해도 Cut 버튼은 절대 활성화 되지 않습니다.
 
Button -> TextBox->Grid->Window
 
Tree의 Root에 이르기까지 아무런 ApplicationCommands.Cut에 바인딩 되어있는 객체를 찾지 못했으므로 Cut버튼이 활성화 되지 않는 것입니다. 이를 해결하기 위해서는? 예, 맞습니다. Button을 Window와 다른 Focus Scope내에 두어야 합니다. 다음과 같은 코드로 이것이 가능합니다.
<Button Command="ApplicationCommands.Cut" FocusManager.IsFocusScope="True">Cut</Button>
 
다시 프로그램을 실행하면 기대하는 결과를 볼 수 있을 겁니다.
 
Command에 대한 이슈는 이것이 끝이 아닙니다. 앞으로 두 번 정도 더 포스팅 할 계획인데, 다음 포스팅에서는 DelegateCommand라는 것을 소개할 것이고, 그 다음 포스팅에서는 Command가 event에 비해 가지는 장점에 대해 알아보도록 하겠습니다.
신고

WPF Command -Part2 : RoutedCommand

프로그래밍 2009.01.20 21:02 Posted by 아일레프
  • 이 포스트는 기본적인 RoutedCommand의 바인딩과 CommandBinding을 통한 Handler 등록 방법을 알고 있을 때 이해하기 쉽습니다.

이전 Post에서 만든 Command 객체는 치명적인 문제를 가지고 있었죠. 그 문제를 살펴보면

  1. Command Object와 Command Target이 분리되어있지 않다.
  2. Command를 실행했을 때의 작업을 수정하려면 Command 객체를 수정하고 재 컴파일 해야한다.

결국 1,2번 문제 모두가 Command Object와 Command Target이 분리되어있지 않기 때문에 생기는 문제입니다. Command Object와 Target을 분리하는 가장 간단한 방법은 무엇일까요? 예, 맞습니다. 바로 Event를 사용하는 것이죠. Command Object가 명령을 받았을 때의 행동을 직접 정의하는 것이 아니라 단지 Command가 실행되었을 때 Command Object가 이벤트를 발생시키고 해당 이벤트를 구독하는 녀석이 실제 명령을 수행하게 하는 것입니다. 그리고 이 이벤트를 구독하는 녀석이 바로 Command Target이 되는 것입니다. 누구나 쉽게 생각할 수 있는 이 방법을 통해 WPF는 RoutedCommand라는 녀석을 미리 구현해 놓았습니다. 그런데 저에게는 그렇게 쉽지 않더군요 ^^;; 

RoutedCommand라는 녀석의 이름답게 RoutedCommand는 RoutedEvent를 사용합니다. RoutedEvent를 사용해 자신의 Command Target을 찾는 것입니다. 다음과 같은 Visual Object Tree를 생각해봅시다.

 Button에 특정 RoutedCommand가 바인딩 되어있다고 합시다.(Button이 Invoker가 됩니다.) 위Button이 눌리면 RoutedCommand 에서 ExecuteEvent를 발생시킵니다. 이렇게 발생된 이벤트는 Button -> Grid -> Windows 순으로 Routing되는 것입니다. 좀 더 일반적으로 말하자면 Command Invoker부터 Visual Tree Root까지 ExecuteEvent가 Routing됩니다.

여기까지는 쉽죠? 예 쉽습니다.

그러면 적절한 시기에 Command 객체의 "CanExecute" 메소드에 대해 알아볼까요? 이 녀석은 일반적으로 Command가 현재 실행 가능한지 가능하지 Command Invoker에게 알려주는 역할을 합니다. 예를 들어 RoutedCommand에 바인딩 된 Command Invoker객체는 자신이 Render되었을 때 RoutedCommand의 CanExecute 메소드를 실행시킨 뒤 그 결과가 true라면 자신을 활성화 시키고 그렇지 않으면 비활성화 시킵니다.

여기까지도 쉬운가요? 쉬웠으면 좋겠습니다.

 그런데 RoutedCommand에서 사용되는 RoutedEvent와 일반 RoutedEvent와의 차이점은 여기서부터 시작됩니다. 이것에 대해 설명하기 위해 이전 포스트에서 사용했던 맥도날드의 Command Pattern을 살펴보기로 하죠.  

CanExecute

자, 주문하는 사람은 Command Invoker, 웨이터는 Command Object, 햄버거 만드는 사람은 Command Target입니다. 주문하는 사람은 웨이터에게 먼저 이렇게 물어볼 것 입니다. "빅맥 햄버거를 먹을 수 있나요?" 웨이터는 현재 빅맥 햄버거를 만들 수 있는 지 알아야 합니다. 때문에 햄버거 만드는 사람들에게 물어보겠죠. "지금 빅맥 햄버거를 만들 수 있는 사람 있습니까?" 중요한 것은 빅맥 햄버거를 만들 수 있는 사람이 적어도 한 명 있으면 주문하는 사람에게 "예, 지금 햄버거를 만들 수 있어요"라고 말한다는 사실 입니다.

자, 이전에 살펴본 Visual Object Tree입니다. 이전과 마찬가지로 Button에 특정 RoutedCommand가 바인딩 되어있다고 가정해 보겠습니다. Button이 Render되면 자신이 실행 가능한 녀석인지 알기 위해 RoutedCommand의 CanExecute 메소드를 실행합니다. 그리고 RoutedCommand는 CanExecute 이벤트를 발생시키죠. 이 이벤트의 Handler의 시그니쳐는 다음과 같습니다.

void CanExecuteRoutedEventHandler(object sender, CanExecuteRoutedEventArgs e)

이 이벤트는 Button -> Grid -> Windows로 타고 올라갑니다. 그리고 만약 Handler 중에서 CanExecuteRoutedEventArgs의 CanExecute Property를 true로 만드는 녀석이 적어도 하나라도 있다면 RoutedCommand는 Button에게 "난 실행될 수 있는 Command야"라고 알려주게 됩니다. 적어도 하나라는 사실을 꼭 기억해 주시기 바랍니다.  

Execute

자 이제 맥도날드에서 주문을 해볼까요? 여러분은 웨이터(Command Object)를 통해 빅맥을 주문할 수 있다는 사실을 알았습니다.(CanExecute 메소드를 통해) 여러분이 빅맥을 주문하면 웨이터는 햄버거를 만드는 사람에게 햄버거를 만들라고 할 것입니다. 주의 해야하는 사실은 웨이터가 단 한 명의 사람에게 햄버거를 만들라고 해야 한다는 사실입니다. 만약 여러 명에게 햄버거를 만들라고 한다면 주문한 햄버거는 하나인데, 여러 개의 햄버거가 만들어져 있겠죠? RoutedCommand도 마찬가지 입니다. Button이 눌러지면 RoutedCommand의 Execute메소드가 실행되고 RoutedCommand는 ExecuteEvent RoutedEvent를 Command Invoker Node부터 발생시킬 것입니다.

이 경우도 Button -> Grid -> Windows순으로 ExecuteEvent가 발생하게 됩니다. 중요한 것은 특정 Command Target의 ExecuteEventHandler가 해당 Command를 처리한다면 e.handled를 true로 만들어 더 이상 Routing을 진행하지 않는 다는 것입니다. 즉 단 하나의 ExecuteEvent Handler만 실행된다는 사실이죠.

MSDN Magazine의 Understanding RoutedEvent and Command는 이 것을 다음과 같이 설명하고 있습니다.

The difference between routed commands and routed events is in how the command gets routed from the command invoker to the command handler. Specifically, routed events are used under the covers to route messages between the command invokers and the command handlers (through the command binding that hooks it into the visual tree).

There could be a many-to-many relationship here, but only one command handler will actually be active at any given time. The active command handler is determined by a combination of where the command invoker and command handler are in the visual tree, and where the focus is in the UI. Routed events are used to call the active command handler to ask whether the command should be enabled, as well as to invoke the command handler's Executed method handler.

위의 굵은 글씨로 표시된 부분이 바로 이를 설명하고 있는 것입니다.

위의 글에는 몇 가지 추가적인 내용이 있는데요, CommandHadler를 선택할 때 CommandInvoker의 VisualTree내의 위치와 현재 Focus가 어느 UI에 있는 지를 고려한다고 합니다. 이 부분은 다음 포스팅에서 설명하도록 하겠습니다.

신고

WPF Command - Part1

프로그래밍 2009.01.12 19:18 Posted by 아일레프

들어가기 전에

얼마 전에 MSDN MagazineUnderstanding RoutedEvent and Command라는 제목의 article이 올라왔습니다. RoutedEvent Command는 자주 사용했던 기능이기에 깊게 읽어보지는 않았습니다. “이 정도는 나도 알고 있다고라는 마음이었죠. 몇 줄 읽어보고 말았습니다..

그 후 Composite Application Guidance for WPF를 공부하기 시작했는데 “DelegateCommand<T>”라는 녀석이 나오더군요. 정말 전혀 모르겠더군요. Command에 대해 정확한 개념이 없다는 것을 알게 된 후 MSDN Magazine Command 부분을 다시 읽어 보았는데 영어가 암호문처럼 느껴지면서 적지 않은 절망감을 느꼈습니다. Article을 이해하기 위해 출퇴근 하는 지하철 안에서 몇 번을 읽었는지 모릅니다. 그리고 집에 와 Command Pattern에 대한 자료와 예제를 반복해 보았습니다. 만약 여러분이 저와 같은 고생을 하지 않고 간단히 WPF Command를 책하나 혹은 MSDN만으로 이해했다면 여러분을 낳아주신 부모님께 감사드릴 일입니다. 그리고 이 Post를 더 이상 읽을 필요도 없을 것입니다.

그러나 Command라는 노래만을 듣고 악보를 그려낼 만할 음감은 없지만 알고 싶다는 호기심과 끈기가 있는 분이라면 이 Post를 읽어주셨으면 합니다.(저는 이런 분들을 더 좋아합니다.) 여러분께 조금의 도움은 될 수 있을 것입니다.

서론은 그만두고, 본격적으로 포스팅을 하도록 하겠습니다. 바라건대 기본적인 RoutedCommand 사용법(CommandBinding, InputBinding) RoutedEvent를 공부하신 후 Magazine Article Understandeing RoutedEvent and Command을 읽어보시기 바랍니다. 그리고 궁금한 부분이나 도무지 풀리지 않을 때 이 포스트를 읽으시면 될 것 같습니다. 그럼 시작해보겠습니다.(해당 Article이 이해가 완전히 되시는 분은 이 포스트을 읽지 않으셔도 됩니다.)

 

Command Overview

RoutedCommand에 대해 알아보기 전에 Command에 대해 알아보도록 하겠습니다. Command1-특정 작업을 서술하는 녀석 그리고 그 2-특정 작업을 요청하는 녀석과 3-특정 작업을 처리하는 녀석을 분리하기 위해 그 필요성이 생겼습니다. ( 1,2,3번은 중요합니다. 모든 커맨드를 사용할 때 항상 1,2,3번에 해당하는 녀석이 무엇인지 마음속으로 추측 해보시기 바랍니다.)

보통 특정 작업을 서술하는 녀석을 Command Object라고 합니다. 그리고 특정 작업을 요청하는 녀석은 Command Invoker 혹은 Command Source, 특정 작업을 처리하는 녀석을 Command Target이라고 합니다. 정리하고 넘어가죠.

1.     특정 작업을 서술하는 녀석 – Command Object

2.     특정 작업을 요청하는 녀석 – Command Invoker, Command Source

3.     특정 작업을 처리하는 녀석 – Command Target


 여러분이 맥도널드에서 특정 햄버거를 주문한다고 가정해보죠. 여러분은 Command Invoker가 되는 것이고, 햄버거 주문을 받는 웨이터는 Command Object, 직접 햄버거를 만드는 사람은 Command Target이 되는 것입니다. 여러분이 햄버거를 주문했을 때 그 내부에서 햄버거를 어떻게 만드는 지 전혀 알지 못해도 되는 이유는 맥도널드가 이 Command Pattern을 사용하고 있기 때문입니다.

 

잠깐 MSDN Article의 예를 보고 넘어가도록 하겠습니다.

<Button Command=”ApplicationCommands.Save”>SAVE</Button>

 

<UserControl …>

           <UserCotrol.CommandBindings>

                     <CommandBinding Command=”ApplicationCommands.Save”

                       CanExecute=”OnCanExecute” Executed=”OnExecute”/>

           </UserControl.CommandBindings>

 

Public class MyControl : UserControl

{

           Private void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

                   e.CanExecute = true;

           }

 

           Private void OnExecute(object sender, ExecuteRoutedEventArgs e)

        {

                   DoSaveOperation();

           }

           Private void DoSaveOperation()

        {

                  

           }

}

위 예에서 1,2,3번에 해당하는 것이 무엇인지 알아보도록 할까요?

1.     Command Object : “ApplicationCommands.Save” static Command 객체

2.     Command Invoker : Button 객체

3.     Command Target : MyControl 객체

 
 맞추셨습니까? 너무 쉽다 구요? ^^;;; 다행입니다. 이것이 기본입니다. 다시 강조 드리지만 반드시 1,2,3번에 해당하는 녀석이 무엇인지 머리 속으로 쭉 생각하시기 바랍니다.

더 나가서 위 예제가 무엇을 하는 녀석인지 볼까요? WPF에는 미리 정의된 Command객체들이 있습니다. ApplicationCommand.Save도 그 중 하나입니다. Button을 누르면(Invoker가 작업을 요청하면) ApplicationCommand.Save Command Target을 찾습니다.(어떻게 찾는지는 다음 Posting에 설명드리겠습니다.) Command Target으로 MyControl 객체가 선택되고 DoSaveOperation()을 실행합니다.

 보시다시피 작업을 요청하는 녀석과 작업을 기술하는 녀석, 작업을 실행하는 녀석이 분리되어있습니다. 이런 것을 CommandPattern이라 부른답니다.

 

Command 만들어보기

위 예제의 세부적인 내용은 일단 미뤄두고 Command객체를 만들어 보도록 하겠습니다. Command 객체를 만들기 위해서는 ICommand라는 Interface를 상속받으면 됩니다. Interface는 이렇게 생겼습니다.

public interface ICommand

    {

event EventHandler CanExecuteChanged;

bool CanExecute(object parameter);

void Execute(object parameter);

    }

CanExecuteChanged이벤트와 CanExecute메소드는 잘 모르더라도 Execute 메소드의 역할은 알 것 같죠? 아마도 Invoker가 이 Command Invoke하면 Execute가 실행될 것 같습니다. 이 정도의 정보만으로도 충분합니다. 한번 AddCommand라는 녀석을 만들어보겠습니다.

public class AddCommand : ICommand

    {

        private string description ="Add";

        public string Description

        {

            get { return description; }

        }

        public bool CanExecute(object parameter)

        {

            return true;

        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)

        {

            System.Diagnostics.Debug.WriteLine("Add명령 실행");

        }

    }

CanExecute메소드는 일단 true를 반환하게 하고 Execute 메소드는 “Add명령 실행이라는 문자열을 출력하게끔 했습니다. 그리고 Description이라는 Property를 만들어 Command의 내용을 기술하게 했습니다. , 그럼 이 Command를 사용해볼까요?

<Window x:Class="CommandTest.Window1"

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

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

    xmlns:loacl="clr-namespace:CommandTest"

    Title="Window1" Height="300" Width="300">

    <Window.Resources>

        <loacl:AddCommand x:Key="AddCommand"/>

    </Window.Resources>

    <Button Command="{StaticResource AddCommand}"

            Content="{Binding RelativeSource={RelativeSource Self}

                              ,Path=Command.Description}"></Button>

</Window>

먼저 Command객체를 XAML로 생성하기 위해 local이라는 prefix namespace를 추가했습니다. 그리고 객체를 생성해 Resources에 등록 했습니다. . 제가 여러분을 너무 무시하나요? 나머지는 그냥 스킵 하겠습니다. 하여간 위 프로그램을 실행하면

 

위와 같은 창이 생기게 되고 Add버튼을 누르면 “ADD 명령 실행이라는 문자열이 Output창에 출력될 것 입니다. 훌륭하군요. 제가 생각하는 결과가 실행되었습니다. 혹시 글을 읽으시면서 Command Object, Command Invoker, Command Target이 무엇인지 생각해보셨습니까? 그랬다면 여러분은 멋진 분입니다. 1,2,3에 해당하는 것이 무엇이지 알아봅시다.

1.     Command Object : AddCommand 객체

2.     Command Invoker : Button 객체

3.     Command Target : AddCommand 객체

? Command Object Command Target이 같네요. 분리되지 않았습니다. 게다가 방금 제가 만든 Command객체는 Command를 실행한 후의 작업이 항상 “Add 명령 실행이라는 문자열을 실행하게 됩니다. 이 작업을 수정하려면 Command객체를 수정하고 다시 컴파일 해야겠네요. 이건 전혀 훌륭하지 않습니다. 망했습니다. .

n  문제 : 어떻게 Command Object Command Target을 분리하지?

다음 장에는 WPF에서 RoutedCommand를 사용해 어떻게 Command Object Command Target을 분리하는 지 알아보도록 하겠습니다. 여러분도 이 문제를 어떻게 해결할 지 생각해보세요 ^^;;


신고
TAG Command, WPF

ListView Sorting

프로그래밍 2008.12.15 21:36 Posted by 아일레프

MSDN이나 여타 블로그를 보면 ListView Colum값을 기준으로 Sorting 하는 방법이 나옵니다.
List
의 순서는 그대로 두고 Row들을 Sorting하는 것 같은데요, 만약 List의 순서를 보존해야 할 필요성이 없다면 List자체를 Sorting하는 것도 한 방법일 것 같습니다
.

일단 Sort를 구현 해야 겠죠? 보통 ListView ItemsSource BindingList를 많이 사용하기에 확장 메소드로 QuickSort라는 녀석을 만들었습니다.


public static class Sort

    {

public static void QuickSort<T>(this BindingList<T>  list,Comparison<T> comparer)

        {

            QuickSort<T>(list, comparer, 0, list.Count-1);

        }

 

        private static void QuickSort<T>(BindingList<T> list,Comparison<T> comparer, int startIndex, int endIndex)

        {

            if (startIndex < endIndex)

            {

                int q = Partition<T>(list, comparer, startIndex, endIndex);

                QuickSort<T>(list, comparer, startIndex, q - 1);

                QuickSort<T>(list, comparer, q + 1, endIndex);

            }

        }

 

        private static int Partition<T>(BindingList<T> list,Comparison<T> comparer, int startIndex, int endIndex)

        {

            T item = list[endIndex];

            int i = startIndex - 1;

            for (int j = startIndex; j < endIndex; j++)

            {

                if (comparer(list[j], item) <= 0)

                {

                    i++;

                    Switch<T>(list, i, j);

                }

            }

            Switch<T>(list, i + 1, endIndex);

            return i + 1;

        }

 

        private static void Switch<T>(BindingList<T> list, int i, int j)

        {

            T item = list[i];

            list[i] = list[j];

            list[j] = item;

        }

    }

 
오랜 만에 보는 Quick Sort의 아름다움이란 ㅜ.ㅜ

이제 Sorting을 해주면 됩니다.

그런데 BindingList를 Sorting하는 것 만으로는 ListView의 Row순서가 바뀌지 않습니다. 이 때는 다음과 같이 CollectionView를 Reflesh해주면 됩니다.

ICollectionView collectionView = CollectionViewSource.GetDefaultView(monitoringView.ItemsSource);

collectionView.Refresh();





 

신고

WPF Object Query ~

프로그래밍 2008.12.15 21:14 Posted by 아일레프

JQuery라는 것을 들어 보셨겠지요? 전 이름은 들어 보았는데 어떤 역할을 하는 놈인지는 어제 알았습니다. 그 중에서 어제 살펴본 것은 JQuery 중에서도 가장 기본적인 기능을 하는 셀렉터였는데요, 그 중 $(“:text”)라고 하는 녀석은 모든 text boxes를 선택합니다.

 

$(“:text”).css(“background-color”, “yellow”);

 

위 명령은 모든 Text Boxes 들의 background ColorYellow로 만듭니다.

 

이 정도는 WPF로 쉽게 만들 수 있겠네요 

public static class WPFQueryExtension{

        public static ElementList<T> SelectObject<T>(this DependencyObject current)

where T : DependencyObject

        {

            ElementList<T> elements = new ElementList<T>();

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)

            {

                DependencyObject obj = VisualTreeHelper.GetChild(current, i);

                if (obj is T)

                {

                    elements.Add(obj as T);

                }

                elements.AddRange(obj.SelectObject<T>());

            }

            return elements;

        }

    }

 

    public class ElementList<T> : List<T> where T : DependencyObject

    {

        public void PropertySet(string propertyName, object value)

        {

            foreach (T obj in this)

            {

                obj.GetType().GetProperty(propertyName).SetValue(obj, value, null);

            }

        }

    }

 

조건 문을 추가 하고 싶으면

 

public static ElementList<T> SelectObject<T>(this DependencyObject current, Func<T, bool> func) where T : DependencyObject

        {

            ElementList<T> elements = new ElementList<T>();

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)

            {

                DependencyObject obj = VisualTreeHelper.GetChild(current, i);

                if (obj is T)

                {

                    if (func(obj as T) == true)

                    {

                        elements.Add(obj as T);

                    }

                }

                elements.AddRange(obj.SelectObject<T>(func));

            }

            return elements;

        }

 

 

위의 코드는 아래와 같이 사용 가능 합니다.

 

this.SelectObject<TextBlock>().PropertySet("Text", "test");

this.SelectObject<TextBlock>(s => s.Text == "test").PropertySet("Text", "11");

foreach (TextBlock t in this.SelectObject<TextBlock>().Where(s => s.Text == "11"))

{

t.Text = "33";

}

.. 왠지 너무 시시하네요.. ㅜ.ㅜ 더 재미있는 것도 할 수 있을 것 같은데 ...

신고

XAML Markup Extension

프로그래밍 2008.11.13 19:53 Posted by 아일레프

Dependency Property에 이어서 Markup Extension에 대한 글을 써 보려 합니다. 아래 적은 내용은 모두 MSDN에서 제공하는 내용을 단지 쉽게(?) 풀어 쓴 것에 지나지 않습니다.

사실 XAML Markup Extension은 XAML에서 특정 Object의 Property의 값을 대입하는 하나의 방법에 지나지 않습니다. 이 방법에는 크게 3가지가 있는데 다음과 같습니다.

  1. Using Property Element

<Button>
  <Button.Background>
   
<SolidColorBrush Color="White" />
 
</Button.Background>
</Button>

       2.   Using XML Attribute

<Button Background="White" />

       3.    Using Markup Extension

<Button Background="{StaticResource MyBrush}" />

1번의 경우 Button의 Background Property에 SolidColorBrush Object를 생성해 대입하는 것을 알 수 있습니다. Button button = new Button { Background = new SolidColorBrush(Colors.White) }; 와 같이 처리가 되겠네요.

2번의 경우는 Background에 "White"라는 문자열이 대입되었습니다. Background는 Brush Type인데 String Type에 해당하는 문자열이 대입되었기에 XAML Processor는 다음과 같은 처리 방법에 따라 Property에 적당한 값을 대입하게 됩니다.

  1. Property의 Type이 primitive Type인가? - primitiveType.Parse(string)을 호출 해 대입
  2. Property의 Type이 Enumeration Type인가? – Enum.Parse(typeof(EnumerationType, string)을 호출해 대입
  3. Property의 Type에 대응되는 TypeConverter가 존재하는가? – TypeConverter.ConvertFrom(string)을 호출해 대입

Using Property Element 와 XML Attribute를 사용해 Property의 값을 대입하는 방식에는 커다란 공통점이 있습니다. 만약 Property의 Type이 primitive Type이 아니라면 "생성"과정, 즉 내부적으로 "new" 키워드를 부르는 연산을 수행한다는 것입니다. 그런데 만약 "생성"과정이 아닌, 이미 존재하는 Object에 대한 참조를 Property에 대입하고 싶다면? 아쉽게도 1번과 2번의 방식으로는 결코 Object에 대한 참조를 대입할 수 없고 이런 방식을 고안 할 수 도 없습니다.

  • 기존에 존재하는 Object의 참조자를 Property에 대입하고 싶을 때 기존의 XML문법으로는 이 방법을 고안하기가 쉽지 않다.(적어도 WPF Team은 찾지 못했다.) 이 용도로 사용되는 XAML MarkupExtension의 가장 큰 예는 StaticResource, DynamicResource, TemplateBinding 등이 있습니다.

또 Property에 대입할 Object가 여러 개의 Constructor Argument를 필요로 하거나 혹은 Property 값 설정을 필요로 한다면? 즉 이런 경우입니다.

Button button = new Button { Background = new MyBrush(parameter1, parameter2) }; 혹은

Button button = new Button {Background = new MyBrush { Color=Colors.White, Offset=1}; ( 물론 MyBrush는 Brush를 상속받은 객체이겠지요)

이 경우는 Property Element를 사용하면 해결할 수 있을 수도 있어 보입니다.

<Button>
 
<Button.Background>
    
<my:MyBrush Color="White" Offset="1"/>
 
</Button.Background>
</Button>

 

그러나 XML Attribute 만을 사용해서는 이런 동작을 기존의 XML 문법만을 이용해서 구현하기는 힘들 것 같군요.

  • Property에 대응되는 Object가 하나 이상의 생성 parameter를 필요로 하거나 Property 설정을 필요로 할 때 기존의 XML의 Attribute문법으로는 이 방법을 고안하기 쉽지 않다. 이 용도로 사용되는 XAML MarkupExtension의 가장 큰 예는 Binding이 있습니다.

결국 Microsoft는 기존의 XML 문법을 확장할 수 밖에 없게 되었는데요 그것이 바로 XAML Markup Extension입니다. Extension에는 XML문법에서 확장했다는 의미가 포함 되어 있고 '{'로 시작하고 '}'로 끝나게 됩니다. 이 Markup Extension은 MarkupExtension클래스를 상속받고 ProvideValues라는 abstract Method를 Override해주는 것으로 구현됩니다.

<Button Background="{StaticResource MyBrush}" />
위의 XAML 은 코드는 Button Button button = new Button{Background = new StaticResourceExtension("MyBrush").ProvideValues()}; 와 같은 느낌으로 해석될 수 있습니다.

자주 사용하는 "{x:Null}" Extension은 어떨까요?

<Button Background="{x:Null}">

위 XAML은 Button button= new Button { Background = new NullExtension().ProvideValues() };로 해석될 수 있겠네요. 쉽지요? ^^;;

{x:Null}은 단순히 null을 return하기에 다음과 같이 직접 NullExtension 을 구현할 수도 있습니다.

public class NullExtension : MarkupExtension
{
  
public override object ProvideValue(IServiceProvider serviceProvider)
  
{
      
return null
   
}
}

정말 쉽네요.

StaticResourceExtension은 내부적으로 FindResource를 부르기 때문에 아마도 다음과 같이 구현될 것 같습니다.

public class StaticResourceExtension : MarkupExtension{
   
private object resourceKey;
   
public StaticResourceExtension(object key){
        
resourceKey= key; 
    
}
    public override object ProvideValue(IServiceProvider serviceProvider){ 
       
FindResource(resourceKey); //이 코드는 동작하지 않습니다. 단지 이런 느낌일 것 같다는 추측;;
    }
    public object ResourceKey
   
{
       
get { return resourceKey;} 
        
set { resourceKey =value;}
    
}
}

위에 Constructor Parameter를 사용하고 있다는 것을 확인해 주시기 바랍니다. 만약 {StaticResource MyBrush}로 사용되었다면 MyBrush가 Constructor Parameter가 되는 것입니다. 또한 StaticResourceExtension은 ResourceKey라는 Property도 가지고 있습니다. 이 Property를 통해서도 StaticResoure를 사용할 수 있는데 {StaticResource MyBrush}는 {{StaticResource ResourceKey=MyBrush}와 동일한 의미를 가지는 XAML 코드입니다.

MarkupExtension은 Object이기 때문에 Property Element를 사용해서 XAML을 구성할 수도 있습니다. <Button Background="{StaticResource MyBrush}" />은 다음과 같이 사용할 수도 있다는 것이죠.

<Button >
   
<Button.Background>
         
<StaticResource ResourceKey="MyBrush" /> 
    
</Button.Background>
</Button>

단 보통 Propery Element를 사용한 것의 차이점은 Background Property에 StaticResourceExtension Object가 대입되는 것이 아니라 ProvideValues() 메소드가 불린 결과가 대입된다는 것입니다. , 즉 Background = new StaticResource{ResourceKey="MyBrush"};로 Processing되는 것이 아니라 Background = new StaticResource{ResourceKey="MyBrush"}.ProvideValues()로 Processing된다는 것입니다.

 

기존의 Microsoft가 제공하는 XAML의 기능에 한계를 느낀다면 직접 MarkupExtension을 구현해보는 것도 재미있는 일이될 것 같습니다.

 

사족 : ProvidesValue의 함수에는 IServiceProvider라는 parameter가 들어가는 데요, 이 것으로 여러가지 Service를 사용할 수 있습니다. (저도 자세히 모르겠어요 ㅜ.ㅜ) 가령 앞서 구현한 StaticResourceExtension클래스를 보면 ProvideValue내에서는 FindResource를 사용할 수 없습니다. (FindResource method를 멤버 method로 가지고 있지 않기때문이죠) 이럴 경우에 serviceProvider를 사용하면 여러 메소드를 사용할수 있습니다. 거듭 말씀드리지만 이부분은 저도 잘 몰라서 혹시 이 부분에 대한 지식이 있으신 분들은 공유해주셨으면 합니다.

 

감사합니다.

신고

WPF에서 가장 중요하고도 이해하기 힘든 부분이 이 Dependency Property인 것 같습니다. WPF로 프로젝트를 진행하면서 가장 이해안가는 부분이 이 Dependency Property의 선언이었습니다. MSDN과 기타 WPF책들을 보면 Dependency Property를 public static readonly로 선언하라고 "권장" 하고 있습니다. 어떤 책에는 "관습"이란 단어로 표현하기도 하더군요. 그 이유가 너무 궁금해 Dependency Property를 private 로 선언한 후 사용해 본 적도 있습니다. 그런데 잘 되더군요. ^^;;; 그렇다면 왜 Dependency Property를 public static readonly로 선언하라고 권장하는 것일까요?

어떻게static으로 선언해도 서로 다른 Object는 다른 값을 가질 수 있지?

일단 static으로 선언한다는 것이 가장 이해가 잘 안 갔습니다. Static 으로 선언된 멤버는 정적 메모리에 단 하나만 존재하게 됩니다. Button을 예로 들어 볼까요? Button등 Control에는 NameProperty라는 Dependency Property가 있습니다. 물론, static으로 선언 되어 있지요.

Button button1 = new Button { Name = "button1" };

Button button2 = new Button { Name = "button2" };

Button button3 = new Button { Name = "button3" };

Button button4 = new Button { Name = "button4" };

위와 같은 프로그램이 있다고 생각합시다. Name이란 Property는 NameProperty(Dependency Property)에 대응되는 Property입니다. 누군가 이렇게 질문했다고 합시다. "NameProperty는 static으로 선언 되어 있어. 따라서 위 코드를 실행시킨 후에는 button1, 2,3,4의 Name은 button4로 동일해야 하지 않을까?

여러분은 위 질문이 잘못된 것임을 알고 있습니다. 그런데 왜 그런걸까요? 분명히 NameProperty는 static으로 선언 되어 있는데 말입니다. 그 비밀은 DependencyObject의 SetValue, GetValue가 가지고 있을 것 같습니다. 보통 관습적으로 Dependency Property에 대응되는 Property는 다음과 같이 구현됩니다.

public string Name

{

set{ SetValue(NameProperty, value);}

get { return (string)GetValue(NameProperty);}

}

만약에 SetValue함수가 정적 공간에 value값을 저장한다면 위의 질문처럼 되야 할 것입니다. 그런데 button1,2,3,4의 Name 값이 서로 다르게 나타나는 것으로 보아 DependencyObject의 SetValue함수는 Dependency Property의 value를 정적 공간에 저장하지 않음을 알 수 있습니다. 그렇다면 어디에 저장하는 것일까요?

사실 DependencyObject는 Dependency PropertyValues라는 Dictionary를 가지고 있습니다.

private IDictionary<Dependency Property, object> Dependency PropertyValues;

DependencyObject는 Dependency Property를 키 값으로 해서 실제 value를 dictionary에 저장하는 것을 알 수 있습니다. 이로서 왜 static으로 저장해도 여러 Instance의 동일한 Dependency Property에 대응되는 Property의 value값이 다른지 알 수 있네요. 또한 여기서 다음과 같은 깨달음을 얻을 수도 있겠습니다.

Dependency Property는 실제 Instance의 Property Value를 얻고 저장하기 위한 Key로 사용된다.

왜 public static readonly로 선언하는가?

위에서 우리는 Dependency Property가 static으로 선언되어도 이에 대응되는 Property가 다른 값을 가질 수 있는지 알 수 있었습니다. 또한 Dependency Property는 자신 스스로의 Dependency Property객체에 value를 저장하지 않는다는 사실도 유추 할 수 있었습니다. 그렇다면 Dependency Property는 어떠한 역할을 하는 것일까요? 단지 value를 찾기 위한 Key로 활용되는 거라면 이 녀석이 필요하지 않을 지도 모릅니다. 또한 DependencyObject의 Setvalue함수가 단지 DependencyPropertyValues[DependencyProperty] = value;와 같이 단순하게 구현되었다면 Dependency Property는 필요 없었겠죠. (사실 DependencyObject의 SetValue함수는 실로 복잡해서 도저히 덤빌 욕구가 안나게 만듭니다.)

여러분도 아시다시피 Dependency Property는 실로 엄청난 일을 해냅니다. 부모 tree로 부터의 값 상속 이라던지, 자동 변경 통지 기능, Template 등등.. 이런 일을 해내기 위해 Dependency Property Object가 존재합니다. Dependency Object는 Dependency Property의 값을 결정하기 위해 Dependency Property 객체를 참고 합니다. Dependency Property에 metadata와 이름, owner Type이 필요한 것은 이 때문입니다. DependencyObject의 SetValue함수는 단순히 value를 dictionary에 저장하는 것이 아니라 현재 자신의 환경?에 맞게(자신의 VisualTree의 위치, Style값 등등) 저장하는데 이 때 Dependency Property의 특성 정보가 적극 활용되는 것입니다.

Dependency Object는 Dependency Property의 metaData와 다중 Provider를 이용해 Dependency Property의 최종 값을 결정한다.

Dependency Property는 자신 스스로가 value값을 가지고 있지 않으며 단지 value값을 결정하는데 필요한 정보를 가진다.

그렇다면 왜 Dependency Property가 public static readonly로 선언하라고 권장하는 것일까요? 제가 생각하는 이유는 다음과 같습니다. 먼저 Dependency Property의 정보를 다른 객체에 노출해야 할 필요가 있기 때문입니다. Binding을 예로 들겠습니다.

Binding bind = new Binding();

bind.Source = this;

bind.Path = new PropertyPath(Window.NameProperty);

canvas1.SetBinding(NameProperty, bind);

( 코드는 아무 짝에도 쓸모없는 코드입니다. 절대 쓰지 마세요^^;;;)

Window의 NameProperty를 canvas1의 NameProperty에 binding하고 있는 정말로 쓸데없는 코드군요. 다만 살펴봐야 할 것은 binding을 사용할 때 bind의 Source가 될 Instance와 Path로 사용될 Dependency Property가 필요하다는 것입니다. 아마 Binding을 수행하기 위해 Window의 NameProperty라는 DependencyProperty정보가 필요하기 때문일 것입니다. Binding 뿐만이 아니라 Style, Template, Animation등도 Dependency Property의 정보를 필요로 합니다. 이로서 public 으로 DependencyProperty를 노출하는 이유는 조금 알겠군요. 그런데 왜 굳이 public static으로 선언할까요? 그냥 선언하고 Property나 함수로 노출하면 안될까요? 물론 이렇게 해도 상관없습니다. 만약 여러분이 DependencyProperty를 Interface로 노출시키고 싶다면 private 로 선언하고 Property나 함수로 접근을 허용해도 상관 없습니다. 다만 이럴 경우가 거의 없다는 것이죠. 그리고 여러 Instance를 생성시켜도 Dependency Property객체가 변할 필요성이 없기 때문에 public static readonly로 선언하게 되는 것입니다.

 

Dependency Property, Visual Tree, Routed Event는 WPF를 이루는 가장 중요한 3대 요소 입니다. 특히 Dependency Property는 이해하기 어려우면서도 정말 신기한 놈입니다. WPF를 만든 사람에게 어떻게 이런 생각을 했냐고 물어보고 싶네요 ^^;;

신고

XAML에서 static 변수 사용법

프로그래밍 2008.08.16 16:39 Posted by 아일레프

블로그에 글을 게시한 지도 꽤 많은 시간이 흘렀습니다. 그 동안 많은 일이 있었습니다. 닷넷엑스퍼트란 좋은 회사에 입사했고, 1 주일 동안 교육을 받은 후 바로 프로젝트에 투입되어 매우 즐겁게 프로그래밍하고 있습니다. 즐거운 일을 할 수 있는 전 참 행운아인 것 같습니다. ^^ 서론은 여기까지 하고 XAML 에서 static변수 사용법에 대해서 알아보도록 합시다.


  • x:Static 키워드

    XAML에서는 static 키워드를 사용하기 위해서 x:Static 이라는 키워드를 사용합니다. WPF책들을 살펴보면 x:Static 키워드를 사용한 예를 찾아볼 수 있는데요, 대부분 SystemParameters 클래스 안의 static 변수를 사용하는 예제를 보여줍니다.

Height="{x:Static SystemParameters.PrimaryScreenHeight}"

위는 Height 속성에 SystemPArameters의 PrimaryScreenHeight Static 변수를 대입하는 XAML코드입니다.


  • 이번엔 직접만든 Class에 static 변수를 넣고 그 값을 한번 XAML에 대입해 봅시다.

namespace Test

{

public class CustomStaticClass

{

public static double WindowHeight = 300;

}

}

Height="{x:Static CustomStaticClass.WindowHeight}"

한번 빌드해볼까요? 당연히 에러가 날 것입니다. 에러 메시지를 보면 CustomStaticClass를 찾을 수 없다고 하는 군요. CustomStaticClass가 있는 namespace를 XAML코드 상에서 인식하지 못하기 때문입니다. XAML에서 네임스페이스를 다음과 같이 추가합니다.

xmlns:local="clr-namespace:Test"

그리고 다음과 같이 작성해 봅시다.

Height="{x:Static local:CustomStaticClass.WindowHeight}"

빌드가 성공하고 성공적으로 프로그램이 동작함을 확인 할 수 있을 것입니다.


  • XAML에서 *.resx파일 사용하기

    이제 좀 더 나아가 visualStudio2008에서 제공하는 *.resx파일을 사용해봅시다. *.resx파일은 Static 멤버를 가지는 클래스를 생성하기 때문에 x:Static 키워드로 사용할 수 있을 것 같습니다.

 

Resources.resx파일에 다음과 같이 Test라는 string을 추가해보도록 하겠습니다.

위와 같이 만든 string 리소스는 코드상에서 다음과 같이 접근이 가능합니다.

string test = Properties.Resources.Test;


XAML에서도 한번 해볼까요?

Title="{x:Static local:Properties.Resources.Test}"

안타깝게 빌드 에러가 납니다. GraphicTest.Properties.Resources가 Test라 는 static member를 가지고 있지 않다고 나오는 군요.

Resource 디자이너가 만든 코드를 열어보았습니다.

internal static string Test

{

get {

return ResourceManager.GetString("Test", resourceCulture);

}

}

위와 같이 존재하고 있음을 알 수 있습니다. 왜 안될까요? 아무래도 internal 키워드가 좀 걸리네요. 위를 보니 class 의 키워드도 public 이 아닌 internal로 되어 있습니다. 모두 public으로 바꾸고 실행 해보았습니다. 오, 성공적이네요. Internal 키워드가 역시 문제였던 겁니다. 그런데, XAML에서 resx파일을 사용하려면 매번 위와 같이 internal로 된 keyword를 public으로 바꾸는 작업을 해야한다면.. 그것은 곧 resx파일을 사용할 수 없다는 것과 동일 하겠군요. ResxXFileCodeGenerator가 public 키워드로 된 클래스와 멤버를 제공하면 좋을 텐데요.


  • 정말 다행히도 VisualStudio2008에서는 PublicResXFileCodeGenerator라는 녀석이 있습니다.

    위와 같이 ResXFileCodeGenerator를 PublicResXFileCodeGenerator 로 바꾸고 저장 후 실행하니 잘 되는 군요. WPF에서는 앞으로 이 녀석을 사용해야 할 것 같습니다. ^^


신고
TAG resx, static, WPF