'silverlight'에 해당되는 글 41건

  1. 2009/06/04 Collection 바인딩과 InvalidOperationException (1)
  2. 2009/05/06 Visual Studio에서 XAML을 열 때 [미리보기] 안하기 (1)
  3. 2009/04/08 AttachedProperty와 데이터 바인딩
  4. 2009/01/09 실버라이트에서의 UI 자동화 - Part 2 (쉬운 방법)
  5. 2009/01/09 [번역] 실버라이트에서의 UI Automation - 사용자 인터랙션 시뮬레이션
  6. 2009/01/05 UI Automation에 관련된 글. (1)
  7. 2009/01/02 Dependecy Property에 관한 블로그
  8. 2008/12/29 Silverlight Bugs and Workarounds
  9. 2008/12/22 Custom Panel 만들기. (1)
  10. 2008/12/13 [강좌] ListBox의 상속.
  11. 2008/12/10 [강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.
  12. 2008/12/10 [강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.
  13. 2008/12/09 [강좌] ContentControl?? ContentPresenter
  14. 2008/12/09 [강좌] ListBox 를 사용하자!!
  15. 2008/12/02 ListBox의 Select된 객체 해제하기.(Select취소하기) (1)
  16. 2008/11/25 FontSource 설정시 주의!!! (1)
  17. 2008/11/12 YouCard Re-visited: Implementing the ViewModel pattern 해석
  18. 2008/11/05 Wheel 지원 리스트 박스
  19. 2008/10/27 VisualStudio 에서의 미리 보기 에러
  20. 2008/10/06 Silverlight Unit Test Template (1)
  21. 2008/09/29 팁 : 실버라이트 2 RC0 포팅 시 Style에서 발생하는 오류
  22. 2008/09/19 VisualState 의 동적 제어.
  23. 2008/09/11 실버라이트에서 MD5 암호화
  24. 2008/08/26 님하 커스텀 커서 지원 점... (1)
  25. 2008/08/20 ButtonBase의 Space바 클릭.
  26. 2008/08/18 FireFox에서의 한글로 된 파일 경로.
  27. 2008/08/18 여러개의 VisualState 조작시 오작동
  28. 2008/08/14 (Firefox에서) 실버라이트 런타임 또 깔으라고 나오는 경우 중 하나!
  29. 2008/08/02 [공지] Error 1001 이란? (5)
  30. 2008/07/15 [강좌] ImageButton 만들기.
2009/06/04 21:58

Collection 바인딩과 InvalidOperationException

원문 : http://gilverlight.net/3069

실버라이트 어플리케이션을 개발하다보면,
ListBox의 ItemsSource와 List<>나 Collection<>과 같은 IEnumerable류를 바인딩 할 경우가 자주 있습니다.

휴즈플로우에서 진행한 최근 프로젝트 중에서 MVVM 패턴으로 개발한 어플리케이션이 있는데요.
ListBox와 Collection류의 프로퍼티가 바인딩하게 되는 여러 뷰들을 빠른 속도로 전환하다보면,
InvalidOperationException이 발생하였습니다.



Exception에 담겨있는 에러메세지는 "개체의 현재 상태 때문에 작업이 유효하지 않습니다."라는 애매한 메세지였고,
예외가 발생한 곳은 뷰모델의 베이스용으로 구현해 놓은 ViewModelBase의 OnPropertyChanged(...) 함수 내부였습니다.

어플리케이션을 천천히 여유있게 조작하면 문제가 발생하지 않다가, 악의적인 유저로 돌변하여 UI를 이리저리 정신없이
전환시키다 보면 발생하는 Exception인데 해결하기가 여간 어려운게 아니더군요.

이 애매한 문제는 결국 파티션 너머의 공도씨를 호출하여 도움을 받아 해결하였습니다.

수술 전 코드와 수술 후 코드를 보시면서, 어떤 코드가 더 안전한지 이해하실 겁니다.

수술전
view source
print?
01.public List<PHOTO> Photos
02.{
03.    get
04.    {
05.        return _photos;
06.    }
07.    set
08.    {
09.        _photos = value;
10.  
11.        OnPropertyChanged("Photos");
12.    }
13.}


수술후
view source
print?
01.public List<PHOTO> Photos
02.{
03.    get
04.    {
05.        return _photos;
06.    }
07.    set
08.    {
09.        if (_photos != null)
10.        {
11.            _photos.Clear();
12.            _photos = null;
13.        }
14.  
15.        _photos = value;
16.  
17.        OnPropertyChanged("Photos");
18.    }
19.}


콜렉션을 통째로 새 객체로 덮어쓰더라도 전에 사용하고 있던 콜렉션을 Clear해 주고, 변수를 null로 초기화해주면
이런 일이 발생하지 않습니다. 정말 바인딩에서 발생하는 문제들은 타이밍에 관련된 것도 있고 오묘해서 해결하기가
쉽지 않은데 노련한 공도씨가 단박에 해결해 주었네요.

이런 팁은 내용을 이해한 후에 습관화하는 것이 좋을 것 같습니다.


저작자 표시 비영리 동일 조건 변경 허락
Trackback 0 Comment 1
2009/05/06 09:43

Visual Studio에서 XAML을 열 때 [미리보기] 안하기

원문 : http://gilverlight.net/3053

실버라이트 개발을 하면서 Visual Studio에서 XAML을 열어보실 때 공통적으로 느끼시는
불편함이 하나 있으실 겁니다. 바로 XAML에 대한 뷰가 미리보기 창과 코드 창으로 분할되어 나오면서
미리보기 때문에 PC가 버벅거리는 것!

XAML을 볼 때 기본뷰를 바꿈으로써 이 불편함을 해소할 수 있는데요.
혹시 모르시는 분이 있으실까봐 소개합니다.

[Tools-Options-Text Editor-XAML-Miscellaneous]에 가시면
아래 그림처럼 Always open documents in full XAML view란 옵션을 발견하실 수 있을 거예요.
체크박스를 켜주시고 OK를 눌러 저장하시면, 다음부터 XAML을 열었을 때 쾌적한 환경을 맛보실 수
있으실 겁니다.

(2009년 5월 6일 추가됨 - 시작)
한글판 비주얼 스튜디오에서는 아래와 같이 찾아가시면 된다고 합니다.
[도구-옵션-문자편집기-XAML-기타]에 가시면 기본보기 : 항상 전체 XAML 뷰에서 문서 열기란 옵션을 발견하실 수 있으실 것입니다. <= 네이버 실버라이트 카페 '쥰세'님의 제보
(2009년 5월 6일 추가됨 - 끝)

더 이상 XAML 보기가 부담스럽지 않다!

감사합니다.

저작자 표시 비영리 동일 조건 변경 허락
Trackback 0 Comment 1
2009/04/08 14:48

AttachedProperty와 데이터 바인딩

AttachedProperty는 XAML 코드 상으로는 하나의 영역에 있어 보여도 실은 다르게 취급된다는거...
그래서 AttachedProperty안에 있는 오브젝트에 데이터 바인딩을 걸려면 AttachedProperty에서 Set할 때 안에 들어갈 오브젝트에게 DataContext를 넘겨줘야 함.

...근데 왜 {Binding Source={StaticResource Source}, Path=Property}가 안먹을까... 이건 의문.
저작자 표시 동일 조건 변경 허락
Trackback 0 Comment 0
2009/01/09 10:10

실버라이트에서의 UI 자동화 - Part 2 (쉬운 방법)

-----------------------------------------------------------------------------------------------------------
이전 포스트에서는 실버라이트에서의 자동화와 접근성을 위해 커스텀 컨트롤을 노출하는 방법에 대해 썼습니다. 만약 실버라이트의 자동화에만 관심이 있고 실버라이트 유닛 테스트 프레임 워크가 목적에 적합하지 않다면 좀더 쉬운 방법으로 AutomationPeer 타입을 구현하여 테스트 할 수 있습니다.

  실버라이트 2 Beta2 에서 부터 Xaml안에서 AutomationProperties.AutomationId 속성을 이용하여 자동화시 고유하게, 유니크하게 사용할 타입을 만들 수 있습니다. 
 

   <TextBox AutomationProperties.AutomationId="SearchTextBox" x:Name="SearchText" KeyDown="CheckKey" />

 
AutomationProperties.AutomationId 속성은 Beta2에서는 인텔리센스로 지원되지 않고 "The attachable property 'AutomationId' was not found in type 'AutomationProperties'." 란 에러 매세지가 뜨겠지만 무시하고 넘어가도 됩니다.(역자주: 정식 버전(RTM)에서는 지원이 됩니다.) 
 
  이렇게 한 후 UISpy에서 이 객체는 아래와 같이 보일 것입니다. 

    Identification

    ClassName:    ""

    ControlType:    "ControlType.Custom"

    Culture:    "(null)"

    AutomationId:    "SearchTextBox"

    LocalizedControlType:    "custom"

    Name:    ""

    ProcessId:    "2808 (iexplore)"

    RuntimeId:    "42 197822 3"

    IsPassword:    "False"

    IsControlElement:    "True"

    IsContentElement:    "True"

 

  Visibility

    BoundingRectangle:    "(240, 327, 300, 23)"

    ClickablePoint:    "390,338"

    IsOffscreen:    "False"

 
이 부분에서 한 사용자를 시뮬레이션 한다면 세가지 작업을 예상할 수 있습니다. 

1. ClickablePoint(클릭 가능한 점)으로 MouseCursor를 이동.
2. 마우스 커서를 클릭.
3. 타이핑을 시작.

커서를 클릭 가능한 점으로 이동시키기 위해서는 Cursor.Position 프로퍼티를 설정해주어야 합니다. 이것을 위해서는 System.Windows.Point와 System.Drawing.Point간의 소통이 필요합니다. 여기서는 확장 메서드를 사용하여 아래와 같이 구현하였습니다. 

    public static class ExtensionMethods

    {

        public static System.Drawing.Point ToDrawingPoint(this System.Windows.Point windowsPoint)

        {

            return new System.Drawing.Point

                    {

                        X = Convert.ToInt32(windowsPoint.X),

                        Y = Convert.ToInt32(windowsPoint.Y)

                    };

        }

    }

 
마우스를 움직인 후에는 Click을 구현해야만 합니다. Click을 구현하기 위해서는 DllImport 를 사용해야 합니다. 이것을 사용해 호출 코드를 간단하게 만드는 Wrapper 클래스는 만들 수 있습니다. 

    public static class Mouse

    {

        private const UInt32 MouseEventLeftDown = 0x0002;

        private const UInt32 MouseEventLeftUp = 0x0004;

        [DllImport("user32.dll")]

        private static extern void mouse_event(UInt32 dwFlags, UInt32 dx, UInt32 dy, UInt32 dwData, IntPtr dwExtraInfo);

 

        public static void Click()

        {

            mouse_event(MouseEventLeftDown, 0, 0, 0, IntPtr.Zero);

            mouse_event(MouseEventLeftUp, 0, 0, 0, IntPtr.Zero);

        }

    }

 
이제, Xaml에서 Automation ID를 설정했기 때문에 Automation ID로 객체를 구분할 수 있고 마우스를 객체의 ClickablePoint로 이동할 수 있고 마우스 클릭을 시뮬레이션할 수 있습니다. 이제 이 모든 걸 해봅시다. 
 

        [TestMethod]

        public void TestMethod1()

        {

              // Assumes an existing Internet Explorer process is running and pointed at your Silverlight app

            Process process = System.Diagnostics.Process.GetProcessesByName("iexplore").First();

            AutomationElement browserInstance = System.Windows.Automation.AutomationElement.FromHandle(process.MainWindowHandle);

            Thread.Sleep(1000);

 

            TreeWalker tw = new TreeWalker(new PropertyCondition(AutomationElement.AutomationIdProperty, "SearchTextBox"));

            AutomationElement searchBox = tw.GetFirstChild(browserInstance);

            Thread.Sleep(1000);

 

            System.Windows.Point uiaPoint;

 

            if (searchBox.TryGetClickablePoint(out uiaPoint))

            {

                Cursor.Position = uiaPoint.ToDrawingPoint();

                Mouse.Click();

                SendKeys.SendWait("Hello, world!");

            }

            else

            {

                Assert.Fail();

            }

        }

만약 동적으로 코드에서 컨트롤을 생성한다면 어떻게 해야 할까요? AutomationProperties에 정적 메소드를 사용하여 AutomationId 프로퍼티를 아래와 같이 설정할 수 있습니다. 

    ListBoxItem myDynamicListBoxItem = new ListBoxItem { Content = "Hello, world!" };

    AutomationProperties.SetAutomationId(myDynamicListBoxItem, "myDynamicListBoxAutomationId");

 
SendKeys는 닷넷 프레임워크 3.0에서 소개된 새로운 동작들을 가지고 있습니다. 이런것들이 관심이 있다면 이곳 을 확인 해보십쇼. 만약 위의 결과들이 모순된 행동을 하게 된다면 Test Project의 app.config 화일에 아래 코드를 삽입해 보십쇼.

    <appSettings>

        <add key="SendKeys" value="SendInput"/>

    </appSettings>

 
도움이 되길 바랍니다. 

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

UIAutomation에 대한 두번째 번역이 끝났습니다. 사실 UIAutomation은 테스트 자동화보다는 Accessibility에 초점이 맞혀진 기술이죠. 미국의 경우에는 이미 웹페이지에 대한 접근성에 대한 법률이 재정되었다고 하니 앞으로 이런 부분도 신경을 써야 하지 않을까 싶습니다. 

  프로젝트를 진행하면서 반복되는 리뷸드 그리고 반복되는 타이핑 그리고 반복되는 디버깅을 얼마나 많이 경험했는지 모릅니다. 프로젝트 내부에 아주 조그만한 버그를 잡기 위해 코드를 조금 고치고 프로젝트의 여러  실행단계(로그인 프로세스등등..)를 거쳐서 정말 테스트 하고 싶은 부분을 테스트 한 후 다시 돌아와 디버깅을 해야 하는 경우.. 아무리 앞의 단계를 단순히 한다고해도 정말 짜증나느 일이었죠. 기본적으로 UnitTest화 하여 분리하여 테스트 하는 것이 맞지만 프로젝트를 진행하다보면 그렇게 똑 부러지게 분리되지 않는 경우도 많죠... 그런 경우 이런 테스트 자동화를 통해 어느정도 해결할 수 있지 않을까 합니다. 

  물론 Unit Test 도 이런 UI 테스트 자동화도 추가적인 코드가 들어가고 자칫 번잡해질 수도 있지만 규모가 큰 프로젝트일 수록 복잡한 프로젝트일 수록 미리미리 이런 것들을 준비해놓는 것이 프로젝트 막판에 패닉상태로 빠지지 않는 지름길이 아닐까 합니다.

  Unit Test를 쓰고 안쓰고 UI 테스트 자동화를 하고 안하고는 전적으로 개발자 본인이나 프로젝트 매니저가 선택할 일이지만 일단 사용하려고 하였을 때 제가 번역한 글이 조금이나마 도움이 되길 바랍니다. 

  그럼.. 마지막으로 제가 이 글을 번역하면서 따라한 테스트 프로젝트를 다시 한번 추가 시켜 놓도록 하겠습니다. 

프로젝트를 실행해보는 방법은 먼저 웹프로젝트를 실행시킨뒤 Test를 실행시키시면 됩니다. 그럼..


                                                                                                                                  - smile -




Trackback 0 Comment 0
2009/01/09 08:59

[번역] 실버라이트에서의 UI Automation - 사용자 인터랙션 시뮬레이션


이 글은 다음 링크의 글을 번역한 글입니다.

http://blogs.msdn.com/gisenberg/archive/2008/07/12/ui-automation-in-silverlight-simulating-user-interactions.aspx

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

최근에 저는 한 임시 그룹에서 실버라이트 리치 인터넷 에플리케이션(RIAs)의 자동화에 대한 일을 맡았습니다. 공중에 발표된 몇가지 툴들은 이 점에서 제한된 도움만을 제공해주고 있습니다.  예를 들면 실버라이트는 컨트롤을 제작 중 Unit Test를 사용할 수 있습니다. 다음 링크에서 좀 더 자세한 것을 알 수 있습니다.
http://www.jeff.wilcox.name/2008/03/31/silverlight2-unit-testing/

불행하게도 저의 요구사항은 자동화 시나리오를 가능하게 하는 것입니다. 우리는 RIA의 안 밖에서 마우스를 움직이거나, 어떤 것을 클릭하거나, 제 3자의 인증공급자로 간다거나, 어떤 키들을 입력하는 등의 몇몇 사용자 흐름을 시뮬레이션 해야만 합니다.

실버라이트에서의 UI 자동화는 여기선 뜨겁게 떠오르는 주제가 되었습니다. 실버라이트 Beta2의 발표와 함께 우리는 갈만한 셋길을 발견하기 시작했습니다. 보다 정확히, 우리는 UI자동화를 하는 WPF의 방법을 조금씩 볼 수 있게 되었습니다. 여러분이 만약 이 글을 같이 따라오기 원한다면 UISpy를 설치하셔야 할 것입니다.(http://blogs.msdn.com/windowssdk/archive/2008/02/18/where-is-uispy-exe.aspx).

마이크로소프트 UI 자동화 어셈블리는 .Net 프레임워크 3.0 과 함께 릴리즈 되었습니다. 전통적으로 우리는 실버라이트 에플리케이션와 함께 아주 먼곳에 있지 않은 Microsoft Active Accessibility(MSAA)와 작동하는 다양한 COM 랩퍼(wrappers)를 가지고 있습니다.  만약 WPF에서 UI 자동화를 조금이라도 해보았다면 앞으로 편하게 이해하실 수 있을 겁니다. 그래서 더 깊게 고려하지 않고 바로 UIA 작업을 시작해보도록 하겠습니다.

첫번째로 Visual Studio 2008 에서 새로운 테스트 프로젝트를 시작합니다. 실버라이트에서 UI 자동화 에플리케이션을 시작하기위해 필요한 UIA 네임스페이스는 System.Windows.Automation과 System.Windows.Automation.Providers 입니다. 그리고 관련된 부분을 얻기 위해 닷넷 3.0 이상에 포함된 다음 어셈블리들을 참조로 추가 해야만합니다.

    - UIAutomationProvider.dll

   - UIAutomationClient.dll

   - UIAutomationClientsideProviders.dll

   - UIAutomationTypes.dll

 실버라이트 커스텀 컨트롤의 자동화가 필요하다고 합시다. 컨트롤의 접근가능한 기능들을 조정하기 위한 Peer Type을 반환해주기 위해 (Control 클래스로 부터) OnCreateAutomationPeer 메소드를 오버라이드 해야만 합니다. 접근가능한 기능(?)들은 에플리케이션을 자동화할 수 있게 하는 Key가 될 것이기 때문에 이 작업은 매우 중요합니다.

  텍스트 박스와 검색버튼으로 이루어진 가상의 검색 컨트롤을 가정합니다.

    public partial class SearchBar : Control

    {

       ...

 

        public SearchBar()

        {

            this.GotFocus += (sender, args)

                =>

                {

                    this.SearchText.Focus();

                };

 

            InitializeComponent();

        }

 

       protected override AutomationPeer OnCreateAutomationPeer()

       {

           return new SearchBarAutomationPeer(this);

       }

    }

여기서 Override한 OnCreateAutomationPeer는 접근 가능 기능들을 위한 컨트롤 트리를 감시하는 것(결론적으로 automation 함수들)에 의해 불려지게 될 것입니다. Peer객체는 접근성이 필요한 것에 밀착하는 의미로(?) 컨트롤의 조합을 반환할 책임을 지게됩니다.( The Peer object will be responsible for returning your combination of controls in a manner that is coherent to anything that needs accessibility. )

또 여기서 이 Control의 GotFocus 핸들러를 컨트롤의 default .Focus() 동작에 설정하기 위해 엮어두었습니다.

다음으로 SearchBarAutomationPeer 클래스의 구현 부분을 봅시다.

    public class SearchBarAutomationPeer : FrameworkElementAutomationPeer, IValueProvider

    {

        public SearchBarAutomationPeer(SearchBar searchBar) : base(searchBar)

        {

        }


Peer 클래스는 작업하는데 필요할 모든 메소드를 제공하는 FrameworkElementAutomationPeer 로 부터 상속 받아야 합니다. 그리고 이 컨트롤의 구성요소중 TextBox와의 인터랙션을 위해 IValueProvider 와 맵핑이 필요합니다. 다음 링크에서 Provider 인터페이스와 개개의 구성요소와의 맵핑에 관해 더 알 수 있습니다.
http://msdn.microsoft.com/en-us/library/system.windows.automation.provider.aspx

컨트롤 트리에서 우리 컨트롤을 찾기 위해선 컨트롤에 클래스 이름과 접근 가능한 구별자(accessibility identifier)를 주는 것이 필요합니다. 이것을 하기 위해서는 FrameworkElementAutomationPeer의 GetAutomationIdCore() 와 GetClassNameCore() 함수를 오버라이드 해야만 합니다.

        protected override string GetAutomationIdCore()

        {

            return "SearchBar"; // You're going to want to make this unique. ;)

        }

 

        protected override string GetClassNameCore()

        {

            return "SearchBar";

        }

 

        protected override bool IsKeyboardFocusableCore()

        {

            return true;

        }

IsKeyboardFocusableCore 는 꼭 override해야만 함수로 만약 이 함수가 없다면 컨트롤의 SetFocus() 함수의 호출이 실패하게 될 것입니다.  이제 Provider 인터페이스의 구현에 대해서도 생각해봐야 합니다. 생성자에서 건네 받은 SearchBar는 base.Owner 프로퍼티를 통해 얻어 올 수 있습니다. base.Owner로 부터 SearchBar를 매번 캐스팅 하는 단조로움을 피하기 위해 프로퍼티를 하나 추가할 것입니다.

        public SearchBar SearchBar

        {

            get

            {

                return (SearchBar)base.Owner;

            }

        }

 

        #region IValueProvider Members

 

        public bool IsReadOnly

        {

            get

            {

                return this.SearchBar.SearchText.IsReadOnly;

            }

        }

 

        public void SetValue(string value)

        {

            this.SearchBar.SearchText.Text = value;

        }

 

        public string Value

        {

            get

            {

                return this.SearchBar.SearchText.Text;

            }

        }

 

        #endregion

이제 UISpy로 우리 컨트로을 보게 되면 다음과 같이 볼 수 있을 겁니다.

 

Identification  
    ClassName:    "SearchBar"

    ControlType:    "ControlType.Custom"

    Culture:    "(null)"

    AutomationId:    "SearchBar"

    LocalizedControlType:    "custom"

    Name:    "SearchBar"

    ProcessId:    "2276 (iexplore)"

    RuntimeId:    "42 197110 6"

    IsPassword:    "False"

    IsControlElement:    "True"

    IsContentElement:    "True"

 

  Visibility

    BoundingRectangle:    "(356, 286, 949, 36)"

    ClickablePoint:    "830,304"

    IsOffscreen:    "False"

 

ControlPatterns

  Value

    Value:    ""

    IsReadOnly:    "False"


 

ControlPatterns 아래 "Vaule" 프로퍼티는 자동적으로 IValueProvider 인터페이스로 인해 우리 컨트롤의 TextBox 의 Vaule와 맵핑됩니다. 깔끔하죠?

이제 이 커스텀 컨트롤의 배관이 가능하게 되었습니다. 이제 TestMethod를 살펴봅시다. 

        [TestMethod]

        public void TestMethod1()

        {

            Process process = System.Diagnostics.Process.GetProcessesByName("iexplore").First();

 

            AutomationElement browserInstance = System.Windows.Automation.AutomationElement.FromHandle(process.MainWindowHandle);

            TreeWalker tw = new TreeWalker(new PropertyCondition(AutomationElement.ClassNameProperty, "SearchBar"));

            AutomationElement searchBar = tw.GetFirstChild(browserInstance);

 

            myElement.SetFocus();

            Thread.Sleep(1000);

            searchBar.SetFocus();

            Thread.Sleep(1000);

 

            SendKeys.SendWait("Hello, world!");

        }

이 부분에서 몇가지 질문이 생기는 분이 계실지 모르겠습니다. 예를 들면 "왜 내가 IValueProvider를 구현했을까?". 위의 코드 조각은 사용자 입력을 시뮬레이션 합니다. 먄약 저것이 우리 것이 아니라면 ValuePattern 으로 부터 와야합니다. 개인적으로, 저는 ValuePattern/TryGetCurrentPattern/etc 의 상호작용을 알아냈고 꽤 거추장스러운 전체 경험을 발견했습니다.  밑에 코드에서 제가 뜻하는 바를 알 수 있을 겁니다.

        [TestMethod]

        public void TestMethod1()

        {

            Process process = System.Diagnostics.Process.GetProcessesByName("iexplore").First();

 

            AutomationElement myElement = System.Windows.Automation.AutomationElement.FromHandle(process.MainWindowHandle);

            TreeWalker tw = new TreeWalker(new PropertyCondition(AutomationElement.ClassNameProperty, "SearchBar"));

            AutomationElement searchBar = tw.GetFirstChild(myElement);

 

            object valuePattern;

            searchBar.TryGetCurrentPattern(ValuePattern.Pattern, out valuePattern);

            ((ValuePattern)valuePattern).SetValue("Hello, world!");

        }


 이글이 결코 이해하기 좋은 가이드라인은 아니지만 UI 자동화가 어떻게 진행되는지 관심이 있는 몇몇 분들에게는 충분할 것이라는 생각이 듭니다.

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

 흠냐... 번역이 좀 엉터리인 부분이 있고.. 글도 생각보다 어려워서 좀 걱정이 되는군요. 또 마지막 부분은 저도 이해하지 못한 부분이고 구현도 되지 않아서 여러분의 도움을 요청합니다.^^;;

  일단 따라해본 샘플 프로젝트를 첨부합니다. 



  Part2 가 있어서 조금더 쉽게 구현하는 방법이 소개 되어있으니 그 부분만 따라하셔도 아마 자동화 부분은 해결하실 수 있으실 것이라 봅니다. 그럼..^^

                                                                                                                         - smile -

Trackback 0 Comment 0
2009/01/05 22:34

UI Automation에 관련된 글.


http://blogs.msdn.com/gisenberg/archive/2008/07/17/ui-automation-in-silverlight-part-ii-the-easy-way.aspx

http://blogs.msdn.com/gisenberg/archive/2008/07/12/ui-automation-in-silverlight-simulating-user-interactions.aspx

이제 공부해야지.^^;;
Trackback 0 Comment 1
2009/01/02 10:04

Dependecy Property에 관한 블로그

NinZhang 님이 아주 잘 정리해 놓으셨군요..^^

WPF 에서의 Dependency Property:
Trackback 0 Comment 0
2008/12/29 11:21

Silverlight Bugs and Workarounds

http://blogs.msdn.com/silverlight_sdk/pages/silverlight-bugs-and-workarounds.aspx

잊지 말자 버그들..!!

얼마전에 아무 생각없이 고생한 부분이 있어 이렇게 링크를 걸어놉니다. 

정리하자면.

1. MultiScaleImage.AspectRatio 가 Custom MultiScaleTileSource를 썼을 경우 정확하지 않다는 것.
2. Mac에서 Safari3나 FireFox3로 MBR(Multi Bit Rate) Video Play시 Freezing.
3. Custom Control에 ScrollViewer.VerticalScrollBarVisibility/HorizontalScrollBarVisibility 속성 추가가 안됨.
4. 동적으로 Image 추가,삭제시 메모리 누수현상(제거시 Image.Source = null 로 셋팅)
5. 많은 컨트롤을 동시에 에니메이션할 경우 StackOverFlowException 발생. (Dispatcher.BeginInvoke를 통한 VisualStateManger.GotoState 호출)
6. 낮은 Bandwidth에서 Media를 빠르게 Seeking시의 깨짐 현상.
7. ImageSource나 ImageBrush 에 event 설정시 메모리 누수.(event listener 들 제거)
8. 파이어복스에서는 3GB 이상의 화일은 앞으로 Seek 가 되지 않음.
9. MAC에서는 3.5GB이상의 큰 화일은 Play가 되지 않음.(브라우저 지원문제)
10. BrowserHttpWebRequest 의 AllowReadStreamBuffering 속성이 false로 되어 MediaElement의 Source로 Stream 되는 경우 플레이 되지 않음.(AllowReadStreamBuffering를 true로 설정)
11. FireFox에서 '%'를 쓴 width,height 는 먹지 않음.
12. Brush를 상속받을 순 있으나 사용할 순 없음.(Brush는 상속받아서 쓰지 않는다.)

6번의 경우 얼마전에 저희 프로젝트에서도 문제가 되었는데... 원문에서는 ProgressDown에서 발생한다고 되어있는데실제로 Streaming의 경우에도 발생하였습니다. WorkAround 도 MediaFailed 이벤트시 Source를 다시 설정하고 예전 Position으로 다시 설정하면 된다고 되어있는데 Stream의 경우에는 MediaFailed 이벤트도 들어오지 않고 그냥 Closed 이벤트가 들어오더군요.

  그래서 제가 생각한 WorkAround는 Seek를 빨리 하지 못하도록 하는 것입니다. 한 0.3-0.5 초 정도의 지연만 주면 사용하기도 불편하지 않고 위와 같은 문제도 발생하지 않더군요.

  그럼 모두 삽질 금지..^^

                                                                                                                  - smile -
Trackback 0 Comment 0
2008/12/22 12:59

Custom Panel 만들기.

원래 제가 직접강좌를 쓰려고 했으나.. 외국분이 아주 잘 정리해 놓은 포스트가 있어 번역합니다. (영어공부도 할겸..^^;;)

  원본 링크는 다음과 같습니다. 


그럼 이제 번역을 해보죠. 오역이 있더라도 이해해주시고 피드백 주시길 바랍니다. 

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

  저는 지난 몇주간 CustomPanel을 만드는 방법을 알아내려 많은 시간을 보냈습니다. 처음에 저는 매우 혼란스러웠습니다.  조그만 변경에도 제 Panel의 전체적인 Layout은 심하게 바뀌었습니다. 그러나 이렇게 한동안 어둠속에서 헤맨 끝에 드디어 빛을 본 것 같은 느낌이 들었고 그래서 제가 Custom Panel 에 대한 조언을 줄 수 있는지 알아보려고 합니다..  
 첫번째로 Measuring 과 Arranging 에 대해 알아야만 합니다. Silverlight의 layout 시스템은 Mesure 단계에서 부터 시작합니다. 여기선 각각의 자식 Element들이 layout 시스템으로 자신의 Desired Size(원하는 사이즈: 실제 프로퍼티에 존재하는 이름임으로 앞으로는 구지 번역하지 않겠습니다. ) 를 보고합니다. 여기선 Panel 자신의 보고도 포함됩니다. 그 다음 단계는 Arrange 단계입니다. 여기선 패널 자신의 최종 사이즈를 참고하여 각각의 자식의 경계 영역(Bounding box)의 크기와 위치를 지정합니다. 

  그래서 Custom Panel을 만들기 위해서는 Panel을 상속하고 MeasureOverride 와 ArrangeOverride 메소드를 구현한 클래스를 만들어야만 합니다. 저는 BlockPanel이라고 이름 붙인 Panel을 만들었습니다. 3x3 블럭 엘리먼트를 만들고 각 블럭이 나란히 놓여서 Panel의 오른쪽 끝에 도달했을 때 Wrapping(줄바꿈) 되도록 하였습니다. 그리고 각각의 엘리먼트의 사이즈가 100x100 이 되도록 제한하였습니다.  각 element가 원하는 사이즈를 계산하고 블럭들을 조정하는 좀 더 복잡한 잡업을 하려고 했지만  SDK 예제로써 코드가 너무 복잡해지고 길어졌습니다.  그래서 여기 제 Panel에서 자식들이 어떻게 정열될것인지보여주는 그림이 있습니다. 
자 그럼 이제 코드를 봅시다. 여기 제 Panel의 MesureOverride 메소드가 있습니다. 

public class BlockPanel : Panel

    {

        //First measure all children and return available size of panel

        protected override Size MeasureOverride(SizeavailableSize)

        {

            //Measure each child

            foreach (FrameworkElement child in Children)

            {

                child.Measure(new Size(100,100));

            }

 

            //return the available size

            return availableSize;

        }

 

 

}


  여기선 Layout System이 이 Panel의 가능한 사이즈(availableSize)를 넘겨줍니다. 만약 Panel을 생성할 때 Width나 Height 를 특별히 설정해주었다면 그 값이 바로 여기서 얻을 수 있는 값이  됩니다. MeasureOverride 함수 안에서는, 현재 Panel에서 가능한 사이즈를(이 경우에는 100x100) 각각의 자식들의 Measure 함수로 넘겨줍니다. 제가 봤던 많은 예제에서는 기본적으로 Child가  Panel의 전체 영역을 다 차지 할 수 있는 영역을 말하는, availableSize 가 다시 되넘겨졌습니다.  Measure 함수가 호출된 후 Layout System은 자식들의 DesiredSize 를 결정하게 됩니다. 저는 이부분에 대해 아직 상세하게 모든 부분을 이해하지는 못했지만 대체로 고유사이즈(native size) 와 가능한 사이즈(available size) 중에 작은 부분을 DesiredSize로 결정하는 것 같습니다. 
  그래서 이 BlockPanel의 경우 만약 자식 Rectangle의 사이즈가 200x200 이라면 Desired Size는 100x100으로 설정될 것입니다. 또 만약 Rectangle의 사이즈가 50x50 일면 DesiredSize는 50x50으로 설정될 것입니다.  
(역자주: 이부분에 대해 어떻게 결정되는지에 대한 정확한 문서가 나와있지 않습니다. 다만 막연히 다음 링크를 보시면 DeisiredSize에 대한 설명은 보실 수 있습니다. 
DesiredSize는 실제사이즈나 RenderSize와 다르게 Layout을 구성하는데 필요한 계산 값으로만 작동하는 것같습니다.)

모든 객체를 측정(measure)한 후에 전체 Panel로 availiableSize를 반환합니다. 저는 들어왔던 availableSize를 그대로 반환함으로써 Measure 단계에서 Panel의 사이즈를 전혀 제한 하지 않았습니다. 

 다음으로 Layout System 은 ArrangeOverride를 살펴보게 됩니다. 여기 제 Panel의 ArrangeOverride 함수가 있습니다.

public class BlockPanel : Panel

    {

        //Second arrange all children and return final size of panel

        protected override Size ArrangeOverride(Size finalSize)

        {

            //Get the collection of children

            UIElementCollection mychildren = Children;

 

            //Get the total number of children

            int total = mychildren.Count;

 

            //Calculate the number of 3x3 blocks needed

            int blocks = (int)Math.Ceiling((double)total/9.00);

 

            //Calculate how many 3x3 blocks fit on a row

            int blocksInRow = (int)Math.Floor(finalSize.Width / 300); //assuming blocks of 9 element 300x300

 

            //Arrange children

            int i;

           

            double maxWidth = 0;

            double maxHeight = 0;

            for (i = 0; i < total; i++)

            {

                //Find out which 3x3 block you are in

                int block = FindBlock(i);

 

                //Get (left, top) origin point for your 3x3 block

                Point blockOrigin = GetOrigin(block, blocksInRow,new Size(300,300));

 

                //Get (left, top) origin point for the element inside its 3x3 block

                int numInBlock = i-9*block;

                Point cellOrigin = GetOrigin(numInBlock, 3, newSize(100,100));

 

                //Arrange child

                //Get desired height and width. This will not be larger than 100x100 as set in MeasureOverride.

                double dw = mychildren[i].DesiredSize.Width;

                double dh = mychildren[i].DesiredSize.Height;

                

                mychildren[i].Arrange(new Rect(blockOrigin.X + cellOrigin.X, blockOrigin.Y + cellOrigin.Y, dw, dh));

 

                //Determine the maximum width and height needed for the panel

                maxWidth = Math.Max(blockOrigin.X + 300, maxWidth);

                maxHeight = Math.Max(blockOrigin.Y + 300, maxHeight);

            }

 

 

            //Return final size of the panel

            return new Size(maxWidth,maxHeight);

        }

}


기본적으로 각 객체에 대해 계산해주고 있는 것들입니다. 
  1. 어떤 3x3 블럭에 들어갈 것인가?
  2. 3x3 블럭안에 어떤 구역 숫자가 들어갈 것인가?
  3. 3x3 블럭의 Left,Top 코너 위치
  4. 3x3 블럭안 구역의 Left,Top 코너 위치.
  5. 각 객체의 DesiredSize.

제가 이 모든 정보를 가지고 있어야지만, 저는 각 자식 객체의 경계 영역(bounding box)의 사이즈와 위치를 계산할 수 있습니다. 경계영역은 Rect 값으로 Arrange 함수로 넘겨지게 됩니다. 

  저는 또한 모든 3x3 블럭을 포함하는데 필요한 width, height 값을 추적하였습니다.(역자주:maxWidth와 maxHeight 를 갱신함) 그 후  모든 블럭을 포함할 수 있을만큼 충분히 큰 패널의 사이즈를 최종 사이즈(final size)로 설정해주었습니다. 저는  또한 finalSize를 넘겨줄 수 있었고 Panel이 자신의 부모 Container로 부터 가능한 전체영역을 채우게 될 것입니다.

 그래서 잠시동안 저를 혼란스럽게 했던 몇가지 점을 정리하면:
  1. 우리가 child.Measure를 호출 했을 때, 우리는 자식들이 가능한 사이즈를 넘겨주는 것입니다. 자식들의 실제 사이즈를 설정해주는 것이 아닙니다. 
  2. Measure 함수를 호출 한 뒤에 Layout System 은 Element의 DesiredSize를 결정할 것입니다. 다시 말해서 제가 말할 수 있는 것은 우리가 직접 DesiredSize를 설정할 수 있는 방법은 없다는 것입니다..
  3. 우리가 child.Arrange 를 호출 했을 때 우리는 child의 최종 사이즈(final size)를 설정하는 것이 아닙니다. 저는 잠시동안 이것이 child의 사이즈를 설정하는 것이라고 착각했지만 이것은 child를 포함할 경계 영역(bounding box)를 설정하는 것입니다. 그래서 만약 child가 경계영역보다 크다면 그것은 Clip 될 것이며 만약 child가 더 작다면 그것은 기본 정렬(역자주: 보통은 Center 정열)이나 당신이 정해준 정렬에 기반하여 경계 영역안에 위치될 것입니다. 이 Panel 경우에는 경계 영역이 child의 DesiredSize가 되도록 설정했습니다.
  
  제가 호출 한 ArrangeOverride 함수를 포함한 나머지 코드와 함께 Panel을 생성하고 몇개의 Rectangle을 안에 넣은 Xaml 화일을 첨부합니다. 이 예제에는 제가 아직 이해하지 못한 Layout System에 대한 상세한 것들이 있습니다. 그래서 만약 아직 읽어 보지 않았다면 "Object Positioning and Layout"을 읽어보길 권합니다. 또한 Xaml 안에 새로 만든 Custom Panel을 생성해주기 위해서는 새로운 namespace를 선언하는 것이 필요할 것입니다. 이것에 관한 글은 "Mapping to Custom Classes and Assemblies" 을 확인 해보길 바랍니다.


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


번역이 끝났습니다.   흠.. 이번 번역은 생각보다 긴 문장이 많아서 골치가 조금 아팠습니다. 그래도 전체적으로 잘 정리되어있어서 예제를 보시면서 따라하시면 이해하시는데 큰 무리는 없을 것으로 보입니다. 

  그럼 제가 만들었던 예제도 같이 첨부해 드리죠. 


                                                                                                                                                                                    - smile -

저작자 표시 비영리 동일 조건 변경 허락
Trackback 0 Comment 1
2008/12/13 16:34

[강좌] ListBox의 상속.


  사실 이 강좌의 제목은 ItemsControl의 상속이라고 해야 더 옳습니다. 하지만 ItemsControl를 상속받아서 제대로된 Control을 만든다는 것은 보다 험난한 길이기 때문에 그건 다음 강좌를 기약하고 그냥 ListBox를 통째로 상속받아서 ListBox에서(혹은 ItemsControl에서) 다행히 Protected override 메소드로 접근할 수 있는 메소드들만 건드려 보도록 하겠습니다. 

  사실 보통의 경우, 그냥 데이터만 바인딩만 하는 경우, 이런 경우가 왜 필요하느냐 물으실 수도 있겠지만 실제로 ListBox를 그대로 사용하는 경우는 저희 회사만 해도 한번도 없었다고 해도 과언이 아닙니다. 이왕 기본 컨트롤을 제공할 꺼면 좀더 리치하게 제공해줄 것이지. Asp.net 수준 정도로밖에 안만들어 놨기 때문에 그냥 갔다 쓰면 이게 RIA인지 그냥 WebPage인지 알 수가 없죠.. 아.. 잡소리가 길어졌군요..^^;;

  그럼 Rich 한 ListBox 를 위해 일단  기본 ListBox를 최대한 사용하는 방법을 알아보도록 하겠습니다 .그럼 먼저 제공해주는 override 메소드부터 살펴보죠. ListBox(ItemsControl)에서 새로 제공해주는 override 메소드는 다음과 같습니다.

  • protected virtual bool IsItemItsOwnContainerOverride(object item)
  • protected virtual DependencyObject GetContainerForItemOverride()
  • protected virtual void PrepareContainerForItemOverride(DependencyObject element, object item)
  • protected virtual void OnItemsChanged(NotifyCollectionChangedEventArgs e)
  • protected virtual void ClearContainerForItemOverride(DependencyObject element, object item)

5가지 모두 ListBox에 들어갈 Item에 관련된 메소드들이죠. 각각의 메소드들이 무슨 일을 하는지 알아보죠.

  1. IsItemItesOwnContainerOverride(object item)
      간단히 사용자가 넣어준 아이템이 Container를 가지고 있는지 확인합니다. base.IsItemItesOwnContainerOverride(item) 에서는 item이 ListBoxItem 인지 확인하고 ItemsControl을 바로 상속했을 경우에는 base에서 itemdl UIElement 인지 확인합니다.
       return 값이 false 인 경우 다음에 GetContainerForItemOverride 메소드가 호출되고 true일 경우 PrepareContainerForItemOverride 가 바로 호출 됩니다.
  2. GetContainerForItemOverride()
      Container를 반환해줍니다. base.GetContainerForItemOverride() 에서는 ListBoxItem의 새로운 인스턴스를 반환합니다. 이 때 ItemContainerStyle 이 null 이 아닐 때는 새로 생성되는 ListBoxItem에 ItemContainerStyle의 Style이 적용됩니다.
  3. PrepareContainerForItemOverride(DependencyObject element, object item)
      먼저 파라미터인 element에는 위의 GetContainerForItemOverride() 에서 반환된 ListBoxItem 이나 IsItemItesOwnContainerOverride(item) 의 결과가 true일 경우에는 사용자가 넣어준 ListBoxItem 혹은 ListBoxItem을 상속받은 객체가 들어오게 되어있습니다. 그리고 item 에는 사용자가 넣어준 데이타가 들어오게 되어있습니다. 
      일단 base.PrepareContainerForItemOverride(element, item)에서는 element에 ItemTemplate을 적용해주고 element가 ContentControl인 경우 Content에 item을 엮어주는 작업을 해줍니다. 그것과 함께 ListBox를 상속했을 경우에는(ItemsControl 상속의 경우 제외) 현재 SelectedIndex나 SelectedItem 에 따라 Selection 처리를 해줍니다.
  4. OnItemsChanged(NotifyCollectionChangedEventArgs e)
     
    이 메서드는 사용자가 ItemsSource 의 데이타를 INotifyCollectionChanged 인터페이스를 상속받은 데이타 클래스(ex. ObervableCollection<T>: 이 클래스의 사용방법은 차후에 설명하도록 하겠습니다. )로 했을 경우에만 들어옵니다. INotifyCollectionChanged의 CollectionChanged 이벤트가 발생하였을 경우에 들어옵니다.
  5. ClearContainerForItemOverride(DependencyObject element, object item)
      이 메서드는 생각보다 중요한 메서드입니다. 이 부분은 ListBox에서 Item을 제거할 때 들어옵니다.  실제로 기본 ListBox 에서 이부분에 구현된 코드는 없지만 만약 ListBox를 상속받아 PrepareContainerForItemOverride 함수에서 ListBoxItem 에 이벤트를 엮어주었다든지 List나 Dictionary를 따로 만들어 ListBoxItem 을 관리했다면 이 부분에서 해제시켜주거나 List에서 제외시켜주어야 합니다.

흠냐.. 어렵나요? 개발자는 코드로 말해야 하는 것을 길게 글로 써놓았으니 이해하기 힘드셨다고 해도 할 말이 없습니다. 그럼 이제 코드로 보여드리죠. 보여드릴 예제는 ListBoxItem 에 CheckBox가 추가되어 있는 경우입니다. CheckBox가 ListBox가 추가 되어서 Selected 된 것과 별도로 Checked 된 것인지 아닌지도 알아보고 싶은 것이지요.
(tip:UserControl이나 App.xaml에 Style이 있는 경우 Style에서 이벤트를 걸어도 그 이벤트가 비하인드 코드의 이벤트 핸들러로 들어옵니다. 이런 방법으로 구현을 할 수도 있겠지만 Style과 코드 사이에 의존성을 높여서 재사용성도 떨어지며 차후에 Style 변경시 문제가 생길 수 있어 되도록 사용하지 않는 것이 좋습니다. ) 

  그럼 ListBoxItem 을 먼저 Customizing을 해야 하겠군요. 먼저 CheckableListBoxItem이란 class를 선언하고 ListBoxItem을 상속받습니다.

    public class CheckableListBoxItem: ListBoxItem
    {
    }

그리고 여기서 기본 Style도 조금은 바꾸어 주어야 할 것이므로 DefaultStyle도 생성자에서 설정해주어야 합니다. 다음과 같이

        public CheckableListBoxItem()
        {
            DefaultStyleKey = typeof(CheckableListBoxItem);
        }

이 부분은 나중에 Control을 다 만든 다음에 자주 까먹을 수 있는 부분이니 무조건 처음부터 설정해주도록 합시다. (삽질 방지 습관화!!!) 

그 다음에 TemplatePart 에 CheckBox 하나를 추가해주도록 합시다. CheckBox 객체의 이름은 왠만하면 const 값으로 박아 놓고 쓰는게 좋겠죠. 그래서 다음과 같이 짜놓았습니다. 
(클래스 속성)

[TemplatePart(Name=CheckableListBoxItem.CheckBoxName, Type=(typeof(CheckBox)))]
(내부 코드)
CheckBox _checkBox;
internal const string CheckBoxName = "CheckBox";

그리고 OnApplyTemplate에서 Style에 들어있을 CheckBox를 찾아와야 하겠죠. 그리고 찾아온 CheckBox가 Checked 되었는지 안되었는지 확인하기 위해서 이벤트를 엮어줍니다.
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _checkBox = GetTemplateChild(CheckBoxName) as CheckBox;
            if (_checkBox != null)
            {
                _checkBox.Checked += new RoutedEventHandler(_checkBox_Checked);
                _checkBox.Unchecked += new RoutedEventHandler(_checkBox_Unchecked);
            }
        }

여기서 CheckableListBoxItem 의 Check 상태를 우리가 만들 CheckablelistBox 에 알려줄 수 있는 방법은 두가지가 있습니다. 한가지는 이벤트를 이용하는 것이고 다른 한가지는 ListBoxItem이 ListBox의 참조값을 받아서 ListBox의 메서드를 직접 호출 하는 방법입니다. 둘 다 장단점이 있는데, 전자는 ListBox가 ListBoxItem을 제거 할 때 이벤트도 함께 제거해주어야 메모리가 제대로 해제 될 수 있지만 후자보다 ListBoxItem이 독립적으로 사용이 가능하죠. 후자의 경우 메모리 해제 부분에 크게 신경쓰지 않아도 되지만 CheckableListBoxItem은 반드시 CheckableListBox 의 Item으로만 들어가야 한다는 단점이 있죠. 사실 ListBox와 ListBoxItem은 의존성이 아주 높은 관계이므로 후자로 구현해도 무방하고 실제 구현도 후자로 되어 있지만 일단 전자로 구현을 해보도록 하겠습니다. 

  위의 코드로 다음과 같은 이벤트와 이벤트 Fire 함수를 만들어 주고 Checked와 UnChecked  이벤트 핸들러에 함수를 추가해줍니다.

        public event RoutedEventHandler ItemChecked;
        public event RoutedEventHandler ItemUnchecked;

        void _checkBox_Unchecked(object sender, RoutedEventArgs e)
        {
            FireItemUnchecked(e);
        }
        void _checkBox_Checked(object sender, RoutedEventArgs e)
        {
            FireItemChecked(e);
        }

        internal void FireItemUnchecked(RoutedEventArgs e)
        {
            if (ItemUnchecked != null)
                ItemUnchecked(this, e);
        }
        internal void FireItemChecked(RoutedEventArgs e)
        {
            if (ItemChecked != null)
                ItemChecked(this, e);
        }

 자 그럼 앞에서 배운 override 함수를 활용하여 CheckableListBox를 만들어 보죠. 먼저 앞서 만든 것처럼 CheckableListBox를 만들고 DefaultStyle등을 만들어 줍니다. 그리고 맨먼저 ListBoxItem 이 아닌 CheckableListBox가 ListBoxItem으로 만들어지게 하기 위해 override 함수를 수정해주어야 합니다. 

  먼저 OnItemItsOwnContainerOverride 함수의 경우 ListBoxItem인지 체크해주는 함수입니다. 하지만 여기서는 CheckableListBoxItem이어야 하니.. CheckableListBoxItem이 아닌 경우 Container를 새로 만들어주어야 합니다. 
고로 다음과 같이 짜줍니다. 

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is CheckableListBoxItem);
        }

간단하죠?^^ 다음으로 item이 ChekableListBoxItem이 아닌 경우 새로 Container를 만들어 줘야 하니 GetContainerForItemOverride 함수를 수정해주어야 합니다. 다음과 같이 짭니다. 
        protected override DependencyObject GetContainerForItemOverride()
        {
            CheckableListBoxItem item = new CheckableListBoxItem();
            if (this.ItemContainerStyle != null)
            {
                item.Style = this.ItemContainerStyle;
            }
            return item;
        }

  CheckableListBoxItem을 새로 만들어주고 ItemContainerStyle을 적용해주는 것이죠. 이렇게 하면 다음에 짜줄 PrepareContainerForItemOverride 함수의 DependencyObject 로 CheckableListBoxItem이 들어오는 것을 확인할 수 있습니다. 그러면 이제 PrepareContainerForItemOverride 를 수정해봅시다. 
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            CheckableListBoxItem checkableItem = (element as CheckableListBoxItem);
            if (checkableItem != null)
            {
                checkableItem.ItemChecked += new RoutedEventHandler(checkableItem_ItemChecked);
                checkableItem.ItemUnchecked += new RoutedEventHandler(checkableItem_ItemUnchecked);
            }
        }
  PrepareContainerForItemOverride 함수의 base 부분에는 item에 data를 바인딩 시켜주고 Select에 관한 처리가 포함되어있으니 꼭 불러주도록 해야 합니다. 아니면 귀찮은 코드를 몇줄 더 짜야 겠죠.

그런데 여기서 Checked 된 것이 어떤 것인지 알기 위해서는 Check된 상태를 알려줄 Event와 Check된 객체를 담아둘 List가 하나 필요할 것입니다. 그리고 Check된 객체가 원래 어떤 아이템이었는지도 알아볼 수 있는 Dictionary도 하나 그래서 다음과 같은 Event와 Property 등을 추가합니다.
        public event RoutedEventHandler CheckedItemsChanged;
        public ObservableCollection<object> CheckedItems { get; private set; }
        private Dictionary<CheckableListBoxItem, object> _oDicCheckableListBoxItem;
        public CheckableListBox()
        {
            DefaultStyleKey = typeof(ListBox);
            CheckedItems = new ObservableCollection<object>();
_oDicCheckableListBoxItem = new Dictionary<CheckableListBoxItem, object>();
        }
그리고 Item을 제대로 얻기 위해 PrepareContainerForItemOverride 함수에 다음 함수 한 줄을 더 추가해줍니다.
      _oDicCheckableListBoxItem.Add(checkableItem, item);

그리고 아까 PrepareContainerForItemOverride함수에서 추가시켜주었던 EvnetHandler부분을 건드려 줍니다. 
        void checkableItem_ItemUnchecked(object sender, RoutedEventArgs e)
        {
            object item = _oDicCheckableListBoxItem[(sender as CheckableListBoxItem)];
            if (CheckedItems.Contains(item))
                CheckedItems.Remove(item);
            FireCheckItemsChanged();
        }
        void checkableItem_ItemChecked(object sender, RoutedEventArgs e)
        {
            CheckedItems.Add(_oDicCheckableListBoxItem[(sender as CheckableListBoxItem)]);
            FireCheckItemsChanged();
        }
        private void FireCheckItemsChanged()
        {
            if (CheckedItemsChanged != null)
                CheckedItemsChanged(this, null);
        }

자 그럼 이제 마지막(?) Themes/generic.xaml을 추가해주고 그 곳에 CheckBox가 추가된 CheckableListBoxItem Style을 넣어줍니다. 다음과 같이..
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:MakeCustomListBox="clr-namespace:MakeCustomListBox"
             xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">   
    <Style TargetType="MakeCustomListBox:CheckableListBoxItem" >
        .
        .
        .
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="MakeCustomListBox:CheckableListBoxItem" >
                    <Grid Background="{TemplateBinding Background}">
                        .
                        .
                        .
                        <CheckBox x:Name="CheckBox" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}">
                            <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="Left"   IsHitTestVisible="False"/>
                        </CheckBox>
                        .
                        .
                        .
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

    자 이제 완성입니다!! 그럼 테스트를 해볼까요. 간단히 xaml에는 CheckableListBox와 그냥 ListBox를 넣어주고 Add버튼과 DeleteButton을 넣어주었습니다. 그리고 코드는 다음과 같이..
        public Page()
        {
            InitializeComponent();
            myListBox.ItemsSource = new ObservableCollection<object>(){ "하나","둘","셋"};
            resultListBox.ItemsSource = myListBox.CheckedItems;
        }
        private void AddButton_Click(object sender, RoutedEventArgs e)
        {
            (myListBox.ItemsSource as IList).Add(new Color());
        }
        private void DeleteButton_Click(object sender, RoutedEventArgs e)
        {
            (myListBox.ItemsSource as IList).RemoveAt(0);
        }
  잘 작동하나요?.... 결과를 보면 Add 와 Delete도 잘 일어나고 Check 상태도 잘 들어오는 것을 알 수 있습니다. 

  그런데...!!!!  Check상태로 Delete를 누른 객체가 사라지지 않는다는 것을 알 수 있습니다.... 흠... 그렇습니다. 우리가 사용하지 않은 하나의 override 함수를 더 사용해야 합니다. 앞서 강조했던 ClearContainerForItemOverride 함수입니다. 이 함수에서 엮어주었던 이벤트를 해제시켜주고 참고하고 있던 리스트에서 삭제해주는 작업을 해주어야 합니다. 다음과 같이요.
        protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            base.ClearContainerForItemOverride(element, item);
            CheckableListBoxItem checkableItem = (element as CheckableListBoxItem);
            if (checkableItem != null)
            {
                object item2 = _oDicCheckableListBoxItem[checkableItem];
                if (CheckedItems.Contains(item2))
                {
                    CheckedItems.Remove(item2);
                }
                checkableItem.ItemChecked -= new RoutedEventHandler(checkableItem_ItemChecked);
                checkableItem.ItemUnchecked -= new RoutedEventHandler(checkableItem_ItemUnchecked);
                _oDicCheckableListBoxItem.Remove(checkableItem);
            }

        }

  
  급하게 ListBox를 상속받아서 Customizing 하다보면 항상 이 부분을 놓치기 쉽습니다. 이 부분이 구현이 안되면 생각보다 오작동하는 경우가 많기 때문에 꼭 짜주는 것이 좋습니다. 
 
  흠.. 그림 한장 없이 설명하다보니 이해가 쉽게 되지 않을 수도 있다는 생각이 듭니다. 그림을 넣기 좀 애매한 부분이 있어서.^^;;; 지금 여기서 설명한 method들은 모두 ItemsControl 에서 상속받은 method들이기 때문에 ListBox가 아니라 바로 ItemsControl 을 상속받아 Class를 만들 때에도 적용이 가능한 것들입니다. 이해를 돕기 위해 샘플프로젝트를 첨부합니다. 그럼 모두 삽질 금지!!^^

                                                                                                                            - smile -

Trackback 0 Comment 0
2008/12/10 11:09

[강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.

Item 의 스타일을 변경해주는 방법은 2가지가 있습니다.

바로 ItemContainerStyle 을 바꾸어 주는 방법과 ItemTemplate을 바꾸는 방법이지요.

ItemTemplate 을 바꾸는 방법은 스캇 구슬리 강좌 에 소개 되어있는데요. 기본적으로 Container 안에 내용물을 바꾸는 것으로 볼 수 있죠. 
 
그럼 ItemContainerStyle부터 살펴 봅시다. ItemContainderStyle은 Style을 Value 값으로 받습니다. 아시는 분은 알겠지만 Style은 TargetType 이 필요하죠. 여기서 TargetType은 ListBoxItem 입니다. 결국 ItemContainerStyle은 ListBoxItem의 스타일을 정해주는 프로퍼티라는 것이지요.

 그럼 ListBoxItem의 DefaultStyle을 볼까요?
 
<Style TargetType="ListBoxItem" >
  <Setter Property="Padding" Value="3" />
  <Setter Property="HorizontalContentAlignment" Value="Left" />
  <Setter Property="VerticalContentAlignment" Value="Top" />
  <Setter Property="Background" Value="Transparent" />
  <Setter Property="BorderThickness" Value="1" />
  <Setter Property="TabNavigation" Value="Local" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ListBoxItem" xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
        <Grid Background="{TemplateBinding Background}">
          <vsm:VisualStateManager.VisualStateGroups>
            <vsm:VisualStateGroup x:Name="CommonStates" >
              <vsm:VisualState x:Name="Normal" />
              <vsm:VisualState x:Name="MouseOver">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0" To=".35" />
                </Storyboard>
              </vsm:VisualState>
              <vsm:VisualState x:Name="Disabled">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" Duration="0" To=".55" />
                </Storyboard>
              </vsm:VisualState>
            </vsm:VisualStateGroup>
            <vsm:VisualStateGroup x:Name="SelectionStates" >
              <vsm:VisualState x:Name="Unselected" />
              <vsm:VisualState x:Name="Selected">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty="Opacity" Duration="0" To=".75" />
                </Storyboard>
              </vsm:VisualState>
            </vsm:VisualStateGroup>
            <vsm:VisualStateGroup x:Name="FocusStates" >
              <vsm:VisualState x:Name="Focused">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Visibility" Duration="0">
                    <DiscreteObjectKeyFrame KeyTime="0">
                      <DiscreteObjectKeyFrame.Value>
                        <Visibility>Visible</Visibility>
                      </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </vsm:VisualState>
              <vsm:VisualState x:Name="Unfocused" />
            </vsm:VisualStateGroup>
          </vsm:VisualStateManager.VisualStateGroups>
          <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
          <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
          <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}"  />
          <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed" RadiusX="1" RadiusY="1"  />
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

너무 길죠?^^ 제가 알기 쉽게 Template부분만 간단히 정리해보죠.

      <ControlTemplate TargetType="ListBoxItem">
        <Grid>
          <vsm:VisualStateManager.VisualStateGroups/>
          <ContentPresenter />
        </Grid>
      </ControlTemplate>

 간단하죠?^^ 간단히 말하면 ListBoxItem은 하나의 ContentPresenter를 가지고 있는
ContentControl 이죠. VisualStateManager는 Item 상태를 결정 해주는 것이고요.

 사실 ContentPresenter이 없어도 됩니다. 다만 ItemTemplate이 무용지물이 되어버리죠.구지 ItemsTemplate을 쓰지 않고 Container로 모두 끝내도 된다면 상관없겠죠.

 그리고 특히 Select나 MouseOver에 대해 특별한 애니메이션이 필요하다면 꼭 ItemContainerStyle을 다시 설정해주어야 하겠죠.  또 ItemTemplate은 위의 ListBoxItem Style 중에 <ContentPresenter /> 부분을 대체하는 부분이기 때문에 ListBoxItem의 전체적인 디자인을 바꾸지는 못하겠죠.

결국 간단히

ItemContainerStyle은 ListBoxItem의 Style을 바꾸는 것이고
ItemTemplate은 ListBoxItem의 ContentPresenter 부분을 바꾸는 것입니다.


(이렇게 간단한 걸.. 이렇게.. 길게 어렵게 설명한 것인가....--;;;)

아직 이해를 못하신 분들을 위해 두가지 예제를 보여드리죠.

다음의 경우를 살펴봅시다.

        <ListBox Width="60" x:Name="ItemTemplateSettingListBox">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Ellipse Width="5" Height="5" Margin="5" Fill="Black"/>
                        <TextBlock Text="{Binding }"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>


이 후 비하인드 코드에서 다음과 같이 셋팅해줍시다.
 ItemTemplateSettingListBox.ItemsSource = new List<string>() { "하나", "둘", "셋", "넷" };

결과는 다음과 같죠.
ListBoxItem 안에들어가는 내용물 디자인이 바뀌긴 했지만 MouseOver나 Select시 State 변화는 똑같은 걸 알 수 있죠.

  ItemsContainerStyle 을 바꾸어야 하는데 이것은 조금더 복잡하기 때문에 Blend를 사용하는 것이 좋을 것같습니다. Blend에서 ListBox를 선택한 후 ItemsContainerStyle을 바꿀려고 보니 아래 그림과 같이 메뉴가 없는 걸 알 수 있습니다.

ItemTemplate과 다음장에 배울 ItemsPanel은 수정할 수 있는데 ItemsContainerStyle은 찾아볼 수가 없죠. 오른쪽 프로퍼티 창에는 존재하지만 GUI로 작업할 방법이 없는 듯 싶은데요..그래서 여기선 한가지 우회 방법을 써야만 합니다.
바로 ListBoxItem을 하나 만들어서 그 Style을 만들고 ItemsContainerStyle에 적용하는 것입니다.

  왼쪽 Control 메뉴를 클릭하면 선택할 수 있는 모든 컨트롤이 나옵니다.(아니 "Show All" 이 Check 되어있을 경우만요..)


ListBoxItem을 클릭한 후에 ListBoxItem을 적당한 크기로 생성을 합니다. 그리고 ListBoxItem 의 Style을 조정해 주면 됩니다.

Style을 변경하는 방법은 다른 강좌에서 설명하기로 하고 여기서는 간단히 제가 임의로 수정하도록 하겠습니다.
Style을 다 만들고 나면 오른쪽 Resource Tab메뉴에 다음과 같이 Style이 생겨 있음을 볼 수 있습니다.
그러면 이 등록된 Style을 ListBox의 ItemsContainerStyle에 적용하기만 하면 됩니다. 적용방법은 간단하죠. ListBox선택후 오른쪽 속성창에서 다음 그림에서 보이는 것처럼 설정해주시면 됩니다.
이제 적용된 결과를 테스트 해보시면 ListBox의 Item들이 전혀 다르게 Style이 적용된다는 것을 알 수 있을 겁니다.

그럼 다음에는 ItemsPanel 에 대해 설명하죠. 다음 장에는 좀더 자세히 설명을 해야겠다는 반성을.....--;;

                                                                                                                                           - smile -

p.s. 역시 샘플 프로젝트를 첨부합니다.


 
Trackback 0 Comment 0
2008/12/10 09:00

[강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.

 일단 대략적인 사용법은 아래의 강좌를 참고 하시길 바랍니다.

스캇 구슬리의 영문 강좌
http://weblogs.asp.net/scottgu/pages/silverlight-tutorial-part-5-using-the-listbox-and-databinding-to-display-list-data.aspx

번역.
  http://hoons.kr/board.aspx?Name=sivlerlighttip&board_idx=457368&page=1&Mode=2&BoardIdx=11410


그럼 전 따분한 이론은 별로 좋아하지 않으니 바로 실전으로 들어가보겠습니다.

ListBox의 프로퍼티나 메소드에 대한 설명은 강좌 중간, 중간에 설명하도록 하겠습니다.

그럼 일단 ListBox를 만들어 보죠.

사용자 삽입 이미지

일단 테두리만 보일 뿐 아무것도 보이지 않는군요. 일단 여기에 Source를 셋팅 시켜야 겠죠.
이부분은 코드단에서 해야 하죠. 그럼 Page.xaml.cs 화일로 돌아가서 Item을 셋팅시켜주고 오겠습니다.

  여기서 Item을 셋팅 시켜주는 방법은 2가지가 있는데요 하나씩 알아보도록 하죠.

  먼저  ListBoxItems 에 직접 셋팅해주는 방법입니다. 다음과 같죠.
사용자 삽입 이미지

코드로는 아래와 같죠.
myList.Items.Add(myRect);
myList.Items.Add(myEllipse);


이렇게 아이템을 셋팅했을 때의 좋은 점은 소수의 아이템을 셋팅 시 간편하고 추가시킨 UIElement를 MyList.Items Collection을 통해 직접 가지고 올 수 있다는 것입니다.

그런데 이렇게 Item을 셋팅시켰을 때는 한가지 문제점이 생깁니다. 한번 Item으로 셋팅된 Item은 다시는 다른 곳에 셋팅시킬 수 없다는 것입니다. 이것은 버그성 같기도 하지만 어쨋든 다음과 같은 코드는 에러를 수반합니다.

사용자 삽입 이미지

 이에 대한 것은 다른 글에 포스팅을 이미 했으니 참고 하시길 바랍니다.

http://error1001.com/16

  두번째 방법은 위의 방법보다 정상적인 방법이라고 하겠습니다. 바로 ItemsSource 에 DataClass 의 Collection 을 셋팅하는 방법입니다.

  간단히 정리하면...

  MyListBox.ItemsSource = collection;

  뭐 이렇게 된다는 것이죠.

  여기서 약간은 복잡한 루틴이 들어가는데..  먼저 다음과 같은 코드를 써보죠.
 
  MyList.ItemsSource = new List<int>() { 1,2,3,4,5 };

  결과는 다음과 같습니다.

사용자 삽입 이미지

 내부적으로 이루어지는 코드를 살펴 보면 다음과 같습니다. (아래는 beta1때 공개된 Mix Control 소스입니다. 현재버전과는 조금 상이한 부분이 있습니다. 하지만 내부적인 구현은 비슷합니다.)

        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            ListBoxItem listBoxItem = element as ListBoxItem;
            Debug.Assert(null != listBoxItem);

            listBoxItem.ParentListBox = this;

            bool setContent = true;
            if (listBoxItem != item)
            {
                if (null != ItemTemplate)
                {
                    listBoxItem.ContentTemplate = ItemTemplate;
                }
                else if (!string.IsNullOrEmpty(DisplayMemberPath))
                {
                    Binding binding = new Binding(DisplayMemberPath);
                    binding.Converter = new DisplayMemberValueConverter();
                    listBoxItem.SetBinding(ContentControl.ContentProperty, binding);
                    setContent = false;
                }

                listBoxItem.Item = item;
                if (setContent)
                {
                    listBoxItem.Content = item;
                }
                ObjectToListBoxItem[item] = listBoxItem;
            }
            // Apply ItemContainerStyle
            if ((null != ItemContainerStyle) && (null == listBoxItem.Style))
            {
                listBoxItem.Style = ItemContainerStyle;
            }
                                                        .
                                                        .
                                                        .

여기서 PrepareContainerForItemOverride 함수는 ItemsControl의 Virtual 함수인데

Listbox에 들어갈 Item들을 준비해주는 함수입니다. 아이템의 생성은 다른 부분에서

일어나고 여기서는 이미 생성된 아이템의 스타일이나 프로퍼티 값등을 설정해주는

부분입니다.

  여기서 DependencyObject 로 들어오는 element가 바로 ListBoxItem 에 해당하는

UI 객체에 해당하고 object로 들어오는 item은 바로 사용자가 셋팅해준 collection의

한 item 이 됩니다.

여기서 보면 listBoxItem.Content = item; 이렇게 셋팅해주는 부분이 있는데 이 부분에서

item이 UIElement가 아닐 경우에는 object의 ToString 값이 화면에 표시가 됩니다.


  이부분은 앞서 설명했듯이. ListBoxItem 도 하나의 ContentControl로써 ContentPresenter를 포함하고 있습니다.
ContentControl은 Content로 UIElement가 들어올 경우에는 PlaceHolder 역할을 합니다. 하지만 다른 경우에는 TextBlock으로 대체하게 됩니다. 

  여기서 DataBinding 을 좀더 잘 활용하기 위해서는  ListBox의 ItemTemplate과 ItemContainerStyle 속성을 활용하여야 하는데 이 이야기는 다음강좌에 하도록 하죠. 일단은 의도와는 다르게 벌써 글이 길어져서..^^

                                                                                              - smile -




 


 

 
Trackback 0 Comment 0
2008/12/09 13:00

[강좌] ContentControl?? ContentPresenter


바로 리스트 박스로 들어가자고 하니 초반부터 낙오자가 많을 것같다는 생각이 들더군요. 그래서 아주 기본 컨트롤부터 시작하기로 했습니다.

  ContentControl은 아주 유용한 Control이면서 아주 기본적인 Control입니다. 우리가 알고 있는 대부분의 Control이 ContentControl을 상속받고 있죠. 일단 모든 Button류가 상속하고 있고 ListBoxItem등도 상속하고 있는 클래스입니다. 

   그럼 이것이 도데체 어떤 컨트롤인지 알아보기 위해 한번 화면에 뿌려보도록 하겠습니다.

다음처럼 간단한 코드를 Xaml에 추가해보죠..

<ContentControl Content="This is a ContentControl!!"/>

화면에 단순히 "This is a ContentControl!!" 이라고 뿌려지는 것을 볼 수 있을 겁니다.

뭐야... 그냥 TextBlock 인거야?... 하지만 다음과 같은 코드를 뿌려보면 단번에 뭐하는 녀석인지 알 수 있습니다.

       <ContentControl>
            <ContentControl.Content>
                <Ellipse Width="50" Height="50" Fill="Purple"/>
            </ContentControl.Content>
        </ContentControl>

결과는 단순히 보라색 동그라미가 화면에 뿌려지는 것일 겁니다. (여기서 ContentControl.Content 태그는 생략해도 상관없습니다.)

아하 그러면 결국 이놈은 그냥 Content를 표시해주는 역활을 해주는 것이군요... 

사실 좀더 세부적으로 어떻게 구현되는 가를 살펴보기 위해 ContentControl의 Template을 살펴보도록 하죠.

<ControlTemplate TargetType="ContentControl" >
    <ContentPresenter Content="{TemplateBinding Content}"  ContentTemplate="{TemplateBinding ContentTemplate}" Cursor="{TemplateBinding Cursor}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>


여러가지 속성들이 TemplateBinding 되어있는데 이런 속성들은 그냥 ContentControl의 속성을 ContentPresenter에 셋팅시켜주는 것일 뿐이고 결국은 내부적으로 ContentPresenter만 달랑 들어있는 형상이네요. 

  그럼 이 ContentPresenter 란 놈은 무슨 일을 하는지 알아봐야 겠죠. 아쉽게도 ContentPresenter는 Template도 없고 더 뜯어볼 코드도 없습니다.

  예전 beta1때 공개되었던 코드를 참고하자면 다음과 같습니다. (현재의 CotentPresenter와 다른 점이 있을 수 있습니다. 일단 Text관련 Property들이 대부분 사라졌습니다. 현재는 ContentPresenter에 Content와 ContentTemplate 두가지 속성만이 있습니다. )


  코드를 살펴보면 다음과 같은 DefaultTemplate을 가지고 있는 것을 알 수 있습니다. 

private const string ContentPresenterDefaultTemplate =
            "<ControlTemplate " +
              "xmlns=\"
http://schemas.microsoft.com/client/2007\" " +
              "xmlns:x=\"
http://schemas.microsoft.com/winfx/2006/xaml\" " +
              "xmlns:controls=\"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls\" " +
              "TargetType=\"controls:ContentPresenter\">" +
                "<Grid x:Name=\"RootElement\" " +
                  "Background=\"{TemplateBinding Background}\" " +
                  "Cursor=\"{TemplateBinding Cursor}\">" +
                    "<TextBlock x:Name=\"TextElement\" " +
                      "FontFamily=\"{TemplateBinding FontFamily}\" " +
                      "FontSize=\"{TemplateBinding FontSize}\" " +
                      "FontStretch=\"{TemplateBinding FontStretch}\" " +
                      "FontStyle=\"{TemplateBinding FontStyle}\" " +
                      "FontWeight=\"{TemplateBinding FontWeight}\" " +
                      "Foreground=\"{TemplateBinding Foreground}\" " +
                      "HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\" " +
                      "Padding=\"{TemplateBinding Padding}\" " +
                      "TextAlignment=\"{TemplateBinding TextAlignment}\" " +
                      "TextDecorations=\"{TemplateBinding TextDecorations}\" " +
                      "TextWrapping=\"{TemplateBinding TextWrapping}\" " +
                      "VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\" " +
                      "Visibility=\"Collapsed\" />" +
                "</Grid>" +
            "</ControlTemplate>";

  간단히 Grid 안에 TextBlock 하나 있는 그런 Template인 거죠.

  내부적인 구현을 보면 Content와 ContentTemplate Property Change 시 마다 자신의 DataContext에 Content나 ContentTemplate을 엮어준 후 PrepareContentPresenter라는 메소드를 호출해줍니다. 여기서 PrepareContentPresenter 라는 메소드에서는 다음과 같은 작업을 수행합니다. 
  1. 먼저 Default Template을 통해 들어있던 객체를 Grid로부터 제거 합니다. 
  2. 그리고 새로 받은 Template이 있으면 Template을 Content에 UIElement가 있으면 Content를 엘리먼트에 집어넣어줍니다. 
  3. 새로 셋팅된 Template도 없고 Content가 UIElement도 아닌 경우 Default Template에 있던 TextBlock의 Text값에  Content를 ToString 해서 넣어줍니다.
결국 ContentPresenter는 단순한 PlaceHolder 역활을 해준다는 것입니다. 여기서 한가지 알아보지 않은 것이 ContentTemplate 인데 ContentPresenter에 새로운 Template을 넣어준다고 생각하면 쉬울 것같습니다. 다음과 같은 코드를 실행해 보죠.

        <ContentControl Content="This is a ContentControl!!!">
            <ContentControl.ContentTemplate>
                <DataTemplate>
                    <Grid>
                        <Ellipse Width="200" Height="50" Fill="LightSteelBlue"/>
                        <TextBlock Text="{Binding }" VerticalAlignment="Center" HorizontalAlignment="Center" />
                    </Grid>
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>

결과는  
  요렇습니다. 
여기서 Text="{Binding }" 부분은 ContentControl의 DataContext 바로 Content를 Binding 하겠다는 뜻입니다. 
그러니까 "This is a ContentControl!!!" 요 문장을 Text에 바인딩한 것이죠. 

Content 값이 바뀜에 따라 글자가 얼마든지 바뀔 수 있습니다. 그러면 다음과 같은 시도를 해보면 어떨까요?

        <ContentControl x:Name="contentControl">
           <ContentControl.ContentTemplate>
               <DataTemplate>
                    <Grid>
                       <Ellipse Width="200" Height="50" Fill="LightSteelBlue"/>
                       <TextBlock Text="{Binding Tag}" />
                    </Grid>
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>

그리고 비하인드 코드에 다음과 같은 코드를 추가 시킵니다. 

  public partial class Page : UserControl
  {
        public Page()
        {
            InitializeComponent();
            contentControl.Content = new TestData() { Tag = "This is a ContentControl!!!" };
         }
   }

    public class TestData
   {
        public string Tag { get; set; }
   }

결과는 바로위의 예제와 같습니다. 

이번에는 TextBlock의 Text가 ,Content로 엮인 TestData의 Tag Property와 Binding 이 된 것이죠.

그럼 ContentControl에 대해 이해가 잘 되셨는지 모르겠군요.^^ 

마지막으로 한가지 팁을 알려드리자면. ContentControl은 Grid 나 Popup 같이 DataContext를 줄 수 없는 객체에 바인딩을 하는 용도로도 쓸 수 있어요.   Popup 이나 Grid 등을 ContentControl로 감싸고 ContentControl에 DataContext 값을 주면 되죠.. 이런게 어디 쓸모 있을까 싶겠지만 나중에 복잡한 바인딩 모델을 쓰다보면 불가피하게 써야 하는 경우가 생기더군요. 

  그럼 모두 삽질 덜 하시길..~^^

                                                                                                                         - smile -

p.s. 샘플프로젝트를 첨부합니다. 


Trackback 0 Comment 0
2008/12/09 08:58

[강좌] ListBox 를 사용하자!!

대략적인 강좌 진행은 다음같이 할 예정입니다.

 1. ListBox 의 기본 적인 사용법

 2. ListBox 의 확장.(ListBox 상속 받아 쓰기)

 3. Listbox Clone 만들기.

 4. RichListBox 만들기.

 
 이 중 1, 2, 3 번은 확정이고 4번은 고민중입니다. 아직 구현된 component가 제대로 작동하는지 충분히 테스트 해보지 못했기 때문입니다. 그럼 이제 시작해보죠.


                                                                                             - smile -
Trackback 0 Comment 0
2008/12/02 12:27

ListBox의 Select된 객체 해제하기.(Select취소하기)


ListBox는 많은 프로젝트에서 가장 많이 사용하면서도 쓰기 어려운 컨트롤중에 하나죠. 여기서 가끔 사용하게 되는 것이 이미 Select된 객체를 취소시키는 것입니다. 코드로 Select를 하는 방법은 두가지가 있죠. 하나는 SelectedItem을 이용하는 방법이고 하나는 SelectedIndex를 사용하는 방법입니다. 

  SelectedIndex는 선택된 객체의 순서를 반환해주고 SelectedItem은 선택된 객체의 Binding된 Data 값을 반환해주죠.
셋팅을 해줄 때도 역시 선택할 객체의 Index값을 SelectedIndex에 넣어주거나 우리가 선택하고 싶은 Data를 SelectedItem에 셋팅해줌으로써 Select된 객체를 바꿀 수 있습니다. 

  그리고 선택이 되지 않은 초기 값은 SelctedIndex 는 -1 이며 SelectedItem 은 null 값이 됩니다. 

  그러면 반대로 선택을 해제 시키려면 SelectedIndex 에 -1값을 넣어주거나 SelectedItem에 null값을 집어넣어주면 되겠죠...


   그런데 문제는 이것이 잘 안먹는다는데 있습니다. 간단하게 테스트를 해보죠.


 Xaml 코드에서 ListBox를 하나 생성해준 뒤 이름을 그냥 "list" 라고 넣어두었습니다.

그리고 코드는 다음과 같습니다.


 public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
           list.SelectedItem = null; // 혹은 list.SelectedIndex = -1;
        }

선택하자 마자. Select를 풀어주자는 것이죠. 그런데 결과를 보면 절대로 Select는 풀리지 않죠.. Select가 풀리지 않으니 한번 선택한 객체를 다시 선택 이벤트를 받는 것은 불가능해지는 것이죠... --;; 어쩐다..

해결 방법은 간단합니다. " list.SelectedItem = null;" 요 부분을 다음과 같이 바꿔주면 정상 작동합니다.


if (e.AddedItems.Count != 0)
            list.Dispatcher.BeginInvoke(() => { list.SelectedItem = null; });


내부적인 작동은 알수 없지만 추측해보자면.. SelectionChanged 이벤트가 일어나는 타이밍의 문제가 아닐까 싶습니다.

SelectionChanged가 일어났을때는 아직 SelectedIndex나 SelectedItem 설정에 대한 로직이 아직 진행중인 상태인 거죠.  로직이 완전히 끝났을 때 다시 SelectedIndex를 설정해주어야만 정상작동하게 되는 것이죠. Dispatcher는 현재 UI스레드 작업이 완료되면 그 다음 작업을 실행시켜주는 것이니 현재는 아마 처음 Selection 에대한 작업이 실행될 것이고 이 작업이 끝나면 자동적으로 SelectedIndex나 SelectedItem설정에 대한 로직도 끝나 있는 것이죠. 그리고 그 후에 Disptcher에 등록시켜둔 list.SelectedItem = null 이라는 명령을 수행하게 되면 정상작동하게 되는게 아닐까 합니다.

  순전히 추측일뿐 정확한 이야기는 아닐 수 있습니다. 확인 방법은 ListBox 의 내부 코드를 뜯어보는 수밖에..(사실 예전에 뜯어봤는데 지금 다시 뜯어보기 귀찮아서..--;;)

  일단 간단히 SelectionChanged에서 Select 된 객체를 바꿔주거나 해제시켜주고 싶을 때는 Dispatcher를 사용하면 된다는 것입니다. 

  여기서 좀더 흥미로운 실험을 더 해보도록 하겠습니다. 신기하게도 제가 이사실을 발견하고 Gilbert에게 이 사실에 대해 알려주었을 때 Gilbert군은 그냥 SelectedItem 에 null 값을 넣어주면 아이템이 해제된다고 하더군요.. 그래서 Gilbert가 구현한 코드를 보았습니다. 신기하게도 Dispatcher를 사용하지 않고 정상작동되더군요.

  Gilbert가 구현한 부분은 여러개의 리스트 박스가 있어 여러개의 리스트 박스 중 한개의 리스트 박스에만 Selected Item이 존재하도록 하는 것이었습니다. 그래서 하나의 리스트 박스에서 SelectionChanged이벤트로 Selection이 일어났을 때 다른 ListBox객체들의 SelectedItem 값을 null로 만들어주어서 다른 리스트 박스의 선택값을 해제시켜주는 것이죠.

  Gibert군의 코드에서 이건 정말 잘 작동하였습니다. 그래서 저는 다시 테스트를 해보기로 했습니다. 이번에는 ListBox를 하나더 추가하고 이름을 "list2"라고 지어줬습니다.

   그리고 아래와 같이 코드를 구성했습니다.


  public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
            list2.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list2.SelectionChanged += new SelectionChangedEventHandler(list2_SelectionChanged);
        }
        void list2_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
              list.SelectedItem = null;
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
              list2.SelectedItem = null;
        }

결과는... 일단 처음에 list에 3을 클릭하고 list2의 4를 클릭했을 때 분명히 list의 3이 선택해제가 되었습니다. 오.. 이것은 되는 구나 했지만... 그다음에 바로 오작동이 시작되었습니다. 다시 list의 3을 클릭했을 때 list2의 4는 선택해제가 되지 않았습니다. 뿐만 아니라 그 이후에 list의 3을 클릭해도 list2의 4를 클릭해도 전혀 SelectionChaged 이벤트가 들어오지 않더군요..

  이 오작동을 가지고 Gilbert가 실제로 구현해 놓은 부분에 가서 확인을 해보았습니다. 그런데 정말 신기하게도 이런 오작동도 발견되지 않더군요. 그래서 다음처럼 구현을 해보았습니다.

public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
            list2.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list2.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count == 0)
                return;
            if (sender == list)
            {
                    list2.SelectedItem = null; 
            }
            else
            {

                    list.SelectedItem = null; 
            }
        }

list와 list2에서 같은 EventHandler를 사용하는 것이죠. 이렇게 하는 것이 무슨 차이점이 있는지 알 수 없지만 Gilbert군이 구현해놓은 코드에는 이런식으로 구현이 되어있었습니다.

  결과는.... 정말 잘 작동합니다.... 

  이게 잘 작동하는 이유는 정말 알 수가 없군요...


결국 정리하자면 이렇습니다.

SelectionChaged 이벤트에서 listBox의 Selection을 바꿔주고 싶을 때는 왠만하면 Dispatcher를 쓰자!!!!


그럼 모두 삽질 덜하시길...^^

                                                                                                                      - smile -
Trackback 0 Comment 1
2008/11/25 23:11

FontSource 설정시 주의!!!


FontSource 를 셋팅하는 방법은 다음과 같습니다.

tbText.FontSource = new FontSource(stream);

여기서 stream은 보통 폰트 화일을 압축한 zip화일을 WebClient로 불러와서 설정해주게 되죠.

여기서 주의 할 점.

Font 화일이 화일명이 한글 화일이면 폰트가 제대로 적용안된다는 것입니다.

쉽게 말해.

'나눔고딕.ttf' 이런 화일을 압축해서 "NanumGothic.zip" 화일로 압축했다고 하면.

이 zip 화일의 stream 을 FontSource에다가 넣어준 경우 Font stream을 제대로 얻어오지 못한다는 것입니다.

'NanumGothic.ttf'로 화일명을 변경후 압축해서 WebClient를 통해 Stream을 받으면 정상 작동하게 되죠..^^

그럼 모두 삽질 금지!!!!

-  smile -
Trackback 0 Comment 1
2008/11/12 21:22

YouCard Re-visited: Implementing the ViewModel pattern 해석

제목 : ViewModel 패턴의 적용(Implementing)

Model-View-Control 패턴(MVC 패턴)은 요즘 가장 잘 알려진 패턴일 것입니다. 마이크로소프트도 ASP.NET MVC 프레임워크로 시류에 편승하고 있으며, Ruby on Rails, Django(Python) 그리고 Spring MVC 프레임워크(Java) 같은 다른 유명한 프레임워크 들도 이 유명한 패턴들을 적용하고 있습니다. MVC 패턴은 Request-Response 기반의 웹환경에 잘 맞는 패턴입니다. Request 가 들어옴에 따라 Controller 가 어떤 행동이 이루어져야 하는지 결정을 하고 Model 에게 알려주고, 렌더링에 대한 책임을 View 엔진에게 넘겨줍니다. 

반면 실버라이트는 비록 웹에서 실행되지만 전통적인 웹 애플리켜이션보다는 WPF(Window Presentation Foundation)와 같은 Rich Client 애플리케이션에 가깝습니다. 이런 이유로 애플리케이션을 설계 할 때 다른 방법이 요구 됩니다. 이 포스트에서는 Model-View-ViewModel(MVVM) 패턴,(혹은 Fowler가 부르듯이 the Presentation Model) 에 대해 이야기 하려고 합니다. 저의 YouCard 애플리케이션을 이 패턴으로 리팩토링 함으로써 예시를 보여드리려고 합니다. 

 Jon Gossman 과 Dan Crevier 가  MVVM pattern in a WPF context 에 관해 블로깅 하였고 , 최근 Nikhil Kothari가 "ViewModel Pattern in Silverlight using Behaviors" 란 제목의 훌륭한 포스팅을 올렸습니다. Martin Fowler 역시 the Presentation Model 이란 이름으로 이 패턴에 과한 글을 썼습니다. ViewModel이 실버라이트와 WPF에서 이렇게 관심을 끄는 이유는 바로 이 모델이 실버라이트와 WPF에서 지원하는 강력한 데이터 바인딩의 장점을 얻을 수 있게 해주게 때문입니다. MVVM 패턴의 중요개념중의 하나는 특정한 View (사용자 인터페이스)에 맞춘 Model을 만드는 것입니다. View-Model은 IsDiscountingEnabled나 PageTitle 같은 특별한 필드를 포함하고 있을 겁니다. 그리고 이 필드들은 도메인 모델에 깔려있는 하나이상의 필드 폼에 기반한 것입니다. 이 IsDiscountingEnabled 필드는 로그인한 사용자가 discount(할인)에 관한 권한을 가지고 있는지 없는지에 기반할 것이지만 실제 View 는 이것에 대해 알지도 못하며 관심도 없을 것입니다. View는 오직 View-Model의 IsDiscountingEnabled 필드에만 관심이 있고 도메인 모델(domain model)과는  동기화 되지 않습니다.   사용자가 Apply나 Save 버튼을 누르는 것 같이 특정 순간에 View-Model과 도메인 모델은 동기화가 발생하게 됩니다. 그리고 이 동안에  View와 View-Model은 아주 밀접하게 동기화(highly synchronized) 되게 됩니다. 어떻게 View와 View-Model의 동기화를 적용할 것인지는 사용하는 기술에 의존적이겠지만 Fowler는 Data Binding을 통해 이루어질 수 있다고 제안했습니다. 

"아마도 Presentation 모델에서 가장 귀찮은 일은 Presentation Model과 View를 동기화하는 것일 것이다. 이것은 작성하기에는 간단한 코드지만 난 항상 이 반복적인 지루한 코딩을 최소화하고 싶어진다. 이상적으로 몇몇 종류의 프레임워크가 이런 일을 할 수 있지만 나는 닷넷의 데이터 바인딩과 같은 기술과 함께 나타나길 희망한다."  - Martin Fowler -

 Fowler가 말하고 있는 동기화 코드는 Name-TextBox에서 Person이라는 객체의 Name 프로퍼티로 Value를 전달하는코드를 말합니다. 저는 우리가 모두 이런 코드를 쓰고 있고 이것이 지루하고 반복적인 일이라는데 동의할 것이라고 확신합니다. 고맙게도 데이터 바인딩은 닷넷1.1과 윈도우 폼 때 부터 상당히 향상되어 왔고 WPF와 실버라이트에서는 데이터 바인딩이 동기화를 적용하기 위한 자연스런 선택이 되었습니다. 

YouCard Screenshot
   
제가 이 패턴의 예제로 앞으로 사용할 애플리케이션은 저의 오스트리아 Remix 때 보여준 YouCard application 입니다.  리믹스에서 전 '디자이너를 위한 실버라이트2'에 대해 이야기 했었습니다. 이야기의 중심은 블랜드 2.5를 사용하여 애플리케이션을 디자인하고 만드는 것이었습니다. 애플리케이션의 핵심은 YouCardData 클래스와 데이터 바인딩 되어있는 YouCard  UserControl 입니다. 이 클래스는 Twitter 와 Flickr로 부터 데이터를 받아들이고 애플리케이션의 Model과 View-Model로써 행동하는 기능들을 포함하고 있습니다. 이것은 또한 Twitter터로 부터 Http Request를 이용해 다운받은 Twitter-Feed에 관한 Tweet이나 Name, Bio 같은 View의 특수한 필드도 포함하고 있습니다.  YouCardData 클래스는 일정간격으로 Twitter와 Flickr로 부터 데이터를 다운받는 타이머를 실행시킵니다.  단독책임(Single Responsibility) 같은 좋은 프로그래밍 원칙을 따라 저는 Twitter와 Flickr 기능들을 외부 클래스들로 분리 시킬려고 합니다. 이 클래스들은 Model이 될 것이고 Xml을 다운로드 받고 객체로 파싱하는 책임을 지게 될 것입니다. YouCardData는 ViewModel이 되고 Twitter 와 Flickr 서비스 이용을 위한 책임을 지고, View를 위한 로직과 UI로 부터 필요한 필드를 구성하게 될 것입니다. YourCardData의 본래 디자인이 데이터 바인딩을 위한 강력한 지원과 블래드2.5에서의 디자인 타임 지원을 강조하기 위해 구성되었기 때문에 리팩토링(refactoring) 작업을 시작하기 좋을 것 같습니다. 다음 그림은 현재 디자인(left)과 리팩토링 후의 원하는 디자인을 보여줍니다. 

youcardviewmodeldiagram

첫번째 작업은 외부 서비스를 위한 인터페이스를 정의하는 것입니다. 저는 ITwitter나 IFlickr 인터페이스를 정의하는 대신 IMicroBlog 나 IPhotoService같은 보다 일반적인 이름을 사용하기로 결정했습니다. 만약 우리가 FriendFeed나 Picasa, Windows Live Photo Gallery 같은 서비스를 지원한다고 할 때 이 편이 보다 합리적이라고 생각합니다. 그리고 현재의 코드를 각각의 인터페이스의 구체적인 구현(implementation)으로 리팩토링 할 것입니다.  하나는 실제 온라인 서비스를 위한 것이고 하나는 더미 데이터(역자주: 실제 서비스로 부터 불러오는 데이터가 아닌 직접 코딩, 하드 코딩을 통한 데이터)를 이용한 가짜 구현이 될 것입니다.  앞서 말했듯이 이 애플리케이션은 리믹스의 Creative 트랙을 위해 만들어진 데모이고 블랜드에서의 좋은 디자인 경험을 위해 만들어졌습니다. 때문에 디자이너가 실제 코드가 어떻게 작동하는지 알 수 있게 하기 위해 더미 데이터를 생성해야만 했습니다. 만약 XAML을 사용한 디자이너-개발자 워크플로우의 이점을 활용하려고 한다면, 디자인 툴에서 코드가 어떻게 작동할지 생각해 봐야 합니다. 현재 구현 에서는, 만약 생성자에서 블랜드에서 실행되는 것인지 아닌지를 체크하고 있다면  if-문을 통해서 YouCardData 클래스 안에 더미 데이터를 제공 할 수 있을 것입니다. 만약 블랜드 안에서 실행되고 있다면 YouCardData 클래스는 Twitter와 Flickr 서비스의 가짜 구현부분을 이용할 것이고 브라우저에서 실행되고 있다면 YouCardData안에 timer가 실행되고 실제 구현부분을 통해 Twitter와 Flickr를 통해 실제 데이터를 다운 받기 시작할 것입니다. 이것과 관련된 YouCardData 생성자의 중요한 부분은 다음과 같습니다. 

 Code sample 1

이 코드에는 Dependency Injection(역자주: http://jonas.follesoe.no/YouCardRevisitedImplementingDependencyInjectionInSilverlight.aspx 참고)에 대한 선언이 들어가 있는데 이 것에 대한 내용은 다음 포스트에서 다루도록 하겠습니다. 여기서 현재 애플리케이션 Object 의 Type을 체크합니다. 실제로 실행중인 애플리케이션이 본래 애플리케이션의 객체를 반환할 때 블랜드는 블랜드 자신의 애플리케이션 오브젝트를 반환하게 됩니다. 

 리팩토링이 필요한 애플리케이션의 다음 조각은 메인 사용자 인터페이스(Main User Interface) 입니다. 내가 이해하는 바로는 Card에 관련된 모든 것들입니다. 현재 애플리케이션은 Twitter 사용자 이름을 넣을 TextBox와 Card를 새로 추가할 Add-Button 을 가지고 있습니다. Click-이벤트 핸들러 에서 새로운 YouCard 사용자 컨트롤의 Instance를 생성하고 StackPanel에 추가하게 됩니다. 그리고 YouCard 컨트롤에 의해 발생한 Close-Event를 후킹합니다. 만약 이벤트rk 발생하면 StackPanel로부터 컨트롤을 제거해주게 됩니다. 이 접근은 몇몇 안좋은 점이 있습니다. 첫번째 큰 문제는 View(Xaml페이지의 코드 비하인드)부분에 너무 많은 작동과 로직을 세워야 한다는 것입니다. 예를 들어 디자이너는 코드를 바꾸지 않고는 StackPanel을 FlowPanel로 바꿀 수 있는 방법이 없게됩니다. 그리고 또 많은 애플리케이션 로직이 특별한 UI-Control들과 UI-Event들과 연결되어 있기 때문에 단위 테스트(Unit Test)를 하기도 힘들어 집니다. 

  이런 문제를 해결 하기 위해 YouCardData 의 Observable Collection(역자주: Collection의 Item의  변화를 Event나 바인딩으로 감시할 수 잇는 Collection을 말합니다.) 을 포함한 Users라고 불리는 새로운 View-Model을 만듭니다. 이 Collection은 Items Presenter Control(역자주: ItemsControl을 말합니다.) 와 바인딩 되어있고 ItemsControl은 YouCard 사용자 컨트롤을 Data Template으로 사용하게 됩니다. 

Code sample 2

 이제 두 UI가 View-Models와 바인딩 되는 경우를 통해 데이터를 보여주는 법을 다룰 수 있게 되었습니다. 그러나 이제 반 정도 왔을 뿐입니다. 우리는 사용자가 Card를 추가하고 삭제하는 것을 View-Models와 어떻게 상호작용(Interact) 할 것인지 알아야 합니다. 

YouCard interaction photo

YouCard 애플리케이션에서는 View-Models에 영향을 주는 두개의 사용자 인터랙션이 있습니다. 새로운 사용자를 추가하는 TextBox와 Button, 그리고 List에서 카드를 제거하는 빨간 Button이 있습니다. 사용자는 Card를 추가하기 위해 사용자 이름을 입력하고 엔터를 칠 수도 있고 Add Button을 클릭할 수도 있습니다. Add Button은 사용자 이름이 올바른 값이 들어왔을 때만 활성화 되어야 합니다. 사용자가 Close 버튼을 클릭했을 때는 View-Model의 Collection으로 부터 item을 제거해 주어야 합니다. 이 경우 하나의 View에서 다른 View의 View-Model로의 의사소통이 섞여있습니다. YouCard 사용자 컨트롤은 메인 UI가 리스트로부터 자기자신을 삭제하도록 View-Model에게 알려주어야 합니다. 

가장 확실한 방법은 Add 버튼에 클릭 이벤트 리스너(click event listner)를 추가해주고 View-Model에서 새로운 사용자를 추가하는 메소드를 불러주는 것입니다. 그리고 TextBox의 Text Changed 이벤트를 통해 입력된 값이 적당한 값인지 확인하고 텍스트에 기초한 TextBox가 되도록 활성화(Enable) 혹은 비활성화(Disable) 할 수 있을 것입니다. 그리고 빨간 Button에 Click Event를 받을 수도 있습니다.  메인 View는 이 이벤트를 받아서 View-Model로 부터 상응하는 카드를 제거할 수 있습니다. 이 솔루션의 문제는 역시 또 우리는 디자이너가 소유해야만 하는 View 부분에 로직과 작동들을 쌓아두어야 한다는 것입니다.  또한 이것 역시 유효 법칙(Validation Rules) 같은 Unit Test 등을 하거나 사용자의 추가나 삭제를 Local 머신의 독립적인 저장공간(isolated storage)에 남기는 것을 힘들게 합니다.

그래서 View와 View-Model 사이의 인터랙션 문제를 해결하기 위해 Command 패턴을 적용하려고 합니다. Command 패턴은 하나의 액션을 하나의 Command 객체로 캡슐화 시켜줍니다(encapsulate). Command 객체는 일반적으로 실행 메소드(excute method), 이름(name), 설명(description)과 Command가 실행가능한지에 대한 
정보를 포함하고 있습니다. 하나의 Command가 여러개의 UIElement에 붙을 수 있습니다. 보통 버튼이나 단축키나 메뉴 아이템등을 통해 OpenFile-Command를 실행시키고 싶을 것입니다. WPF에는 Commands를 위한 이런 것들이 내장되어 있지만 실버라이트에는 포함되어 있지 않습니다. 

Nikhil 은  Silverlight behaviors를 통해 Command와 같은 기능을 어떻게 가능하게 할 건지에 대한 좋은 아이디어를 소개하고 있습니다. 이것은 AJAX 작동을 위한 ASP.NET 컨틀롤 확장에서 사용되었던 것과 동일한 컨셉입니다. 그의 첫번째 ViewModel 포스트에서 그는 Xaml로 부터 Command를 실행시키기 위해 다음과 같은 코드를 사용하고 있습니다. 

Code sample 3

그는 부착 프로퍼티(Attached Proeprty) 를 사용하여  Button 에 Search TextBox의 TextProperty를 파라미터로 넘겨주고 View-Model의 Search 메소드를 불러주는 작동을 추가해 주었습니다.  다음 포스팅에서 Nikhil은 Dynamic Language 런타임을 사용하여 어떻게 좀더 간결한 코드를 얻을 수 있는지 보여주고 있습니다. (get the syntax even more compact using the Dynamic Language Runtime)

Code sample 4
이 접근법의 근사한 점은 연결된 Click 이벤트에 어떤 Dynamic Language 도 쓸 수 있다는 것입니다.  이것은 페이지안의 다른 엘리먼트들의 파라미터를 얻거나 메소드를 실행시키는 것도 가능합니다. 이것은 View를 View-Model과 연결시키는데 놀랄만한 융통성을 보여 줍니다. 하지만 이 접근법의 문제점은 실버라이트 애프리케이션의 사이즈에 영향을 주는 Dynamic Language Runtime에 의존한다는 것이고, 이것보다 더 중요한 단점은 블랜드 2.5의 디자인 타임(desind time) 깨뜨린다는 데에 있습니다. 그래서 다른 대안이 필요했습니다. 

 저는  "Silverlight Extensions" project on CodePlex 에서 발견한 좀더 고전적인 Command 패턴을 사용하기로 했습니다. 프로젝트는 Controls와 Helper Class들 그리고 확장 메소드(extension methods)등을 포함하고 있습니다. 지금은 오직 커멘드 패턴 구현 부분에만 관심이 있기 때문에 이 클래스들을 YouCard 프로젝트로 옮기기로 합시다. 여기서 TextBox와 Button에 대한 Xaml 코드는 다음과 같습니다. 

Code sample 5

텍스트 박스는 View-Model에서 Username 프로퍼티와 바인딩되어 있습니다. 두 컨트롤 모두 View-Model의 Username을 파라미터로 받아 AddCard Command를 부릅니다. 

커멘드들은 다음과 같이 정의 됩니다. 

Code sample 6

Static Command 클래스는 애플리케이션의 모든 Command들을 참조로 가지고 있습니다. 새로운 Command 객체가 생성될 때, Command 객체 안의 static Dictionary에 애플리케이션에 의해 만들어진 cashing된 Command 객체가 추가 됩니다.  Xaml에서 CommandService Attribute를 사용할 때 CommandService 클래스는 CommandSubscription 클래스를 사용하여 적당한 Command를 UI 객체와 연결시켜주게 됩니다. 사용자가 AddUser-버튼을 클릭하거나 TextBox에서 enter를 쳤을 경우 CommandSubscription 클래스는 올바른 UI-event를 받게 되고 올바른 Command 객체의 Excuted event를 발생 시키게 됩니다.  이 커맨드가 실행되었을때 특정 액션을 하고 싶은 클래스는 간단히 Command 객체의 Excuted event만 연결해주면 됩니다. 여기선 View-Model 클래스에 카드를 추가하거나 삭제하는 것을 조정해주고자 합니다:

Code sample 7
또한 AddUser 버튼의 활성, 비활성(enabling/disabling) 부분도 언급하는게 좋을 것 같습니다. 이것은 버튼의 IsEnabled 프로퍼티를 View-Model의 IsAddEnabled 프로퍼티와 데이터 바인딩 해줌으로써 구현할 수 있습니다. IsAddEnabled 프로퍼티는 다음과 같습니다. 

mvvp-code8

TextBox는 IsAddEnabled 프로퍼티의 PropertyChanged 이벤트를 발생시킬 UserName 프로퍼티와 바인딩 되어있습니다. 프로퍼티를 얻는 곳(getter,역자주:프로퍼티의 get 부분)에 버튼이 활성화 되어야 하는지 아닌지 결정할 Validation rule 을 적용해주어야 합니다. 

희망차게도 이 글은 어떻게 MVVM 패턴이 사용자 인터페이스와 분리하여 코드를 짤 수 있게 도와주는지에 대한 좋은 예를 보여줍니다.  WPF와 실버라이트에서 지원하는 강력한 데이터 바인딩은 하드 코딩을 통해 View와 View-Model를 동기화 하는 것에 대한 걱정을 덜어줌으로써, 이 패턴을 특별히 흥미롭게 만들어줍니다. 커맨드들을 사용함으로써 구체적인 UI-Element의 이벤트로부터 애플리케이션의 액션들을 분리할 수 있습니다. 이것은 디자이너가 애플리케이션 네의 다양한 액션을 일으키는 UI 객체들을 자유롭게 선택할 수 있게 해줍니다. 무엇보다도 가장 좋은 점은 블랜드에서 지원하는 디자인 타임이 깨지지 않는 상태에서 이 모든 것이 가능하다는 것입니다. 다음 포스트에서는 Dependency Injection을 소개하면서 YouCard 애플리케이션을 좀더 테스트가 쉽게 향상 시킬 것입니다. 



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

역자 후기: 흠냐... 생각보다 포스트가 길어 힘들군요..^^;; 100% 모든 프로젝트에 적용될 수 있는 모델은 아니지만 꽤 이상적인 모델중에 하나인 것같습니다. 무엇보다도 Command 패턴은 복잡하고 불분명한 EventHandler로 부터 코드를 깔끔하게 지킬 수 있는 모델이라는 점에서 참 마음에 듭니다. MVVM 패턴도 친구 Gilber군이 이야기 한데로 Model을 다른 프로젝트로 분리시킬 경우 UI에 대한 독립성이 상당히 높아져서 멀티 플랫폼 환경이나 동일 서비스위의 다양한 애플리케이션 모델을 구축하는데 상당히 유리해 보입니다. 마지막으로 Interface와 더미데이터를 통한 디자이너를 배려한 구현은 개발자와 디자이너의 협업 관계에서 상당히 괜찮은 모델로 보입니다. 

  그럼 모두들 도움 많이 되셨길.. 그리고 좋은 포스트를 남겨준 원작자에게 감사를... Thank you for your great post.!! 

   
Trackback 1 Comment 0
2008/11/05 14:52

Wheel 지원 리스트 박스


간단하게 Wheel이 지원되는 리스트 박스 만드는 방법을 알려드리도록 하죠.^^

먼저 Wheel 을 지원 받을 수 있도록 아래 포스트에 가서 Wheel을 지원할 수 있게 하는 class를 다운 받습니다.
http://cafe.naver.com/mssilverlight/693

아니면 새로 짜도 상관은 없습니다.

그리고 아래와 같은 클래스를 만듭니다.

using System;
using System.Windows;
using System.Windows.Controls;
using HugeFlow.Interface;

namespace HugeFlow.Controls
{
    [TemplatePart(Name = WheelListBox.ElementScrollViewerName, Type = typeof(ScrollViewer))]
    public class WheelListBox : ListBox
    {
        #region ScrollOffset
        ///  
        /// Gets or sets the ScrollOffset possible Value of the double object.
        ///  
        public double ScrollOffset
        {
            get { return (double)GetValue(ScrollOffsetProperty); }
            set { SetValue(ScrollOffsetProperty, value); }
        }

        ///  
        /// Identifies the ScrollOffset dependency property.
        ///  
        public static readonly DependencyProperty ScrollOffsetProperty =
                    DependencyProperty.Register(
                          "ScrollOffset",
                          typeof(double),
                          typeof(WheelListBox),
                          null);
        #endregion ScrollOffset

        public WheelListBox() : base()
        {
            DefaultStyleKey = typeof(ListBox);
            ScrollOffset = 20;
            (new MouseWheelHelper(this)).WheelScroll += new EventHandler<MouseWheelEventArgs>(Wheel_Moved);
        }

        void Wheel_Moved(object sender, MouseWheelEventArgs e)
        {
            e.Handled = true;

            double tempOffset = ElementScrollViewer.VerticalOffset - ScrollOffset * e.Delta;

            if (tempOffset < 0)
                tempOffset = 0;
            else if (tempOffset > ElementScrollViewer.ScrollableHeight)
                tempOffset = ElementScrollViewer.ScrollableHeight;

            ElementScrollViewer.ScrollToVerticalOffset(tempOffset);
        }

        ///  
        /// Identifies the optional ScrollViewer element from the template.
        /// 
        internal ScrollViewer ElementScrollViewer { get; set; }
        private const string ElementScrollViewerName = "ScrollViewer";

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ElementScrollViewer = GetTemplateChild(ElementScrollViewerName) as ScrollViewer;
        }
    }
}

 

어째 간단하게 보이실런지..^^ 쉽게 갔다 쓰는 용으로 프로젝트도 하나 만들어 봤습니다. 첨부합니다. 그럼 유용하게 사용하시길.^^

- smile -
Trackback 0 Comment 0
2008/10/27 14:55

VisualStudio 에서의 미리 보기 에러


비쥬얼 스튜디오를 쓰다보면 가끔 이런 에러가 뜰 때가 있습니다.

Error 1 요소 UserControl1에 알 수 없는 Margin 특성이 있습니다. [Line: 716 Position: 37] D:\DevTest\StyleTest\StyleTest\Page.xaml 6 25 StyleTest

그런데 실행은 잘 되죠.

테스트 결과.

App.Xaml에서 스타일이 정의가 되어 있고, 그 스타일을 사용한 객체를 A라고 하고,
A를 포함한 어떤 유저컨트롤을 B라고 가정합니다.

이때 유저컨트롤 B를 가지고 있는 객체 C를
비주얼 스튜디오에서 미리보기 하는 순간 위와 같은 에러를 반환합니다.



Preview상의 버그이고 프로그램에는 전혀 지장을 안 주는 것 같으니 무시합시다. --;;

                                                                                                                                          - smile -

예제를 첨부하죠.

Trackback 0 Comment 0
2008/10/06 14:29

Silverlight Unit Test Template



dll 은 여기서 다운 받습니다.

이건 클래스 템플릿,  \Documents\Visual Studio 2008\Templates\ItemTemplates\Visual C#\Silverlight 
 요 위치에 붙여 넣습니다.

이건 프로젝트 템플릿, \Documents\Visual Studio 2008\Templates\ProjectTemplates\Visual C#\Silverlight
 요 위치에 붙여 넣습니다.  프로젝트가 위에 dll 위치를 못찾을 수 있으므로 dll을 제대로 추가해준 뒤
다시 export하여 zip 화일을 같은 경로에 붙여 넣어줍시다.


자 이제 유닛테스트로 개발 해봅시다.

                                                                                                                   - smile -
Trackback 0 Comment 1
2008/09/29 21:50

팁 : 실버라이트 2 RC0 포팅 시 Style에서 발생하는 오류


사진출처 : flickr.com

포팅작업 돌입!


얼마전 실버라이트 RC0가 공개되어,
휴즈플로우의 은대리는 이전에 만들어 둔 프로젝트를 포팅하는 작업에 들어갔다.
컴파일과 디버깅을 거듭한 끝에 드디어 컴파일 에러 제로!

근데 실행을 시켜 본 순간, 이게 무슨 문제인가?
App.xaml.cs의 InitializeComponent()에서 런타임 에러가 발생한다.

App.xaml을 열자 잘못된 부분에 밑줄이 그어지면서 VS가 이곳저곳 오류를 보고해준다.

'아... ContentTemplate가 Control 부모를 버리고 FrameworkElement에게 입양 되었었지...'
그 결과 많은 프로퍼티들이 사라졌으므로 오류가 발생하는 것이다.
은대리는 FontStyle 등 밑줄이 그어진 많은 프로퍼티를 XAML 코드에서 삭제해 나갔다.
그리고 VisualTransition의 Duration도 잊지않고 GeneratedDuration으로 바꿔주었다.

오류가 눈앞에서 모두 사라졌다.


마지막 고비

이번엔 프로그램이 뜰까? 은대리는 다시 실행해본다...
다시 또 오류다.

마지막 문제는 Visual Studio가 힌트를 주지 않는다.
은대리의 삽질을 막고자하면 아래의 팁을 알려주라!

App.xaml에서

1. vsm:Style 엔티티를 Style로 Replace 한다. 
2. vsm:Setter 엔티티를 Setter로 Replace 한다.

자, 이제 프로그램이 잘 뜬다.
저작자 표시 비영리 동일 조건 변경 허락
Trackback 0 Comment 0
2008/09/19 20:28

VisualState 의 동적 제어.


일단 VisualStateManager class를 통해서 해당 스테이트의 Storyboard를 가지고 오는 방법입니다.

VisualStateManager.GetVisualStateGroups({객체})[{index1}].States[{index2}].Storyboard

뭐 이런 식입니다.

여기서 주의할 점은 "객체"는 해당 컨트롤을 뜯하는 것이 아닙니다.

VisualStateManger는 각 객체의 Dependecy Attached Property와 비슷한 속성을 가지고 있기 때문에

VisualStateManager가 정의 되어 있는 object가  되겠습니다. 쉽게 말하면.. 다음과 같은 xaml코드가 있을 때

                <ControlTemplate TargetType="controls:DropDownBox">
                    <Grid x:Name="Root">
                        <vsm:VisualStateManager.VisualStateGroups>
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualState x:Name="Normal">
                                    <Storyboard />
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="MouseOver">
                                    <Storyboard/>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                    </Grid>
                </ControlTemplate>

바로 "Root"가 "객체"가 된다는 것이죠. 이 템플릿을 가지고 있는 Control이 아닌..

이렇게 지저분한 방법 말고는 Storyboard에 이름을 붙여주는 방법있습니다.

똑같은 코드에 아래와 같이 Storyboard에 이름을 붙이고

                                <vsm:VisualState x:Name="Normal">
                                    <Storyboard  x:Name="NormalStory"/>
                                </vsm:VisualState>


코드 몇 윗부분 TemplatePart를 선언해주는 부분에

 [TemplatePart(Name = "NormalStory", Type = typeof(Storyboard))]

이와 같은 코드를 추가해주시면

GetTemplateChild("NormalStory")

요런 방법으로 Storyboard를 얻어 올 수 있습니다.

이런식으로 VisualState로 선언된 Storyboard도 동적 제어가 가능합니다.

Trackback 0 Comment 0
2008/09/11 16:38

실버라이트에서 MD5 암호화

http://blogs.msdn.com/jeffwilcox/archive/2008/03/05/silverlight-2-md5-hash-string-provider-implementation.aspx

착한 외국분이 이미 개발해 주셨군요. 감사히 가져다 씁시다.

사용법도 아주 간단합니다.

이건 혹시나 링크가 없어질까봐.. 첨부합니다.



                                                                                                                                - smile -
Trackback 0 Comment 0
2008/08/26 22:13

님하 커스텀 커서 지원 점...

때로는 기본 제공 커서 외에 특별한 형태의 커서를 사용하고 싶을 때가 있는 데요, 예를 들어 돋보기라던가...
W3C의 CSS 표준에서도 다음과 같은 코드를 통해 지원하죠.

<style type="text/css">
body {
cursor: url("http://209.85.62.24/86/165/0/f78500/Normal.cur"), pointer;
}
</style>
물론 지금도 기본 커서를 숨긴 상태에서 마우스를 따라다니는 이미지로 커스텀 커서를 구현할 수 있지만 아래에 깔려있는 오브젝트가 많을 경우 욕나오게 느리고 버벅거리죠.

커서 변경 기능은 CSS표준인데다가 IE와 FF모두 지원하는 만큼 실버라이트 런타임도 "CUR"파일을 사용한 커스텀 커서를 허용해줬으면 좋겠어요.
Trackback 0 Comment 1
2008/08/20 10:17

ButtonBase의 Space바 클릭.


오늘도 또 실버라이트의 치부를 공개하게 되는군요.

어제 저는 원하지도 않는 기능을 실버라이트가 지원하고 있다는 사실을 알게 되었습니다.

바로 SpaceBar Click 입니다. 이것은 ButtonBase를 상속한 모든 객체가 적용됩니다.

실제로 Button에 Focus가 가있는 상태에서만 작동을 해서 문제가 되지 않을 수 있지만 다음에

경우 크게 문제가 됩니다.

Button 클릭으로 어떤 새로운 객체를 띄워야 하는 경우.. 이 경우 만약 버튼이 새로운 객체에 의해

가려진다면 실제 의도상으로는 새로운 객체는 한개만 띄워져야 하지만 SpaceBar를 사용하면 여러번

띄워지는 경우가 생기죠.

  이 경우는 개발자가 부주의 한 탓에 생긴 버그라고 할 수 있죠.. 이런 경우 클릭 이후 Focus를 새로 띄운

객체에게 다시 맞춰줘야겠죠.

  다음의 경우는 버그가 좀더 심각합니다. 간단한 프로젝트를 첨부하니 한번 확인해 보시길 바랍니다.




  테스트 방법은 간단합니다.

Button을 하나 만들고 Click 시 이 버튼의 Visibility 를 Collapsed 시켜줍니다.

그리고 Spacebar를 눌러서 Click 이벤트를 발생시킵니다.


Sys.InvalidOperationException: ManagedRuntimeError error #4004 in control 'Xaml1': System.Exception: COM l1 ”Œ 8œÐ HRESULT E_FAIL $X
http://localhost:61604/ButtonSpaceClickTestWeb/ScriptResource.axd?d=tAAvm8BmByFdAKHmeYf8BNblydHO0228NHDLdU66QIf01HCX-g_tKwK9JPhqOuaxX88rI8w2AeWOzicBqEYveg2&t=2077b8c9
Line 441


  다음과 같은 에러가 발생하는군요...

 이 경우는 명백히 버그로 보이지만 피할 방법은 있습니다.

 이렇게 Button에 Focus가 가있는 상태에서 Visibility를 Collapsed 시켜줄 때는 반드시

Focus를 잃게 만드는 것입니다. 그런데.. Focus를 잃게 만드는 메소드가 없으니 Focus가

가도 상관없는 다른 객체에 Focus가 가도록 설정해두면 됩니다.


  예전에는 Page에서 밖에 KeyEvent를 못받아서 불편하긴 했어도 이런 문제는 없었는데 이제

모든 Control들이 KeyEvent를 받으니 이런 문제가 생기는군요.

   암튼 간단히 결론만 말씀드리자면 다음과 같습니다.
  
Button류들은 사용하지 않을 때 꼭 Focus를 해제 해주어야 한다.

  오늘은 결론이 간단하네요.^^ 그럼 Focus관리 잘하셔서

모두들 삽질 덜하시길..^^

                                                                                                       - smile -
 
Trackback 0 Comment 0
2008/08/18 18:57

FireFox에서의 한글로 된 파일 경로.


이번에 보고할 버그는 굉장히 심각한 버그입니다. 실버라이트에서 일반적인 한글 경로의 처리문제인데요.

"당연히 UTF-8로 인코딩 되는거 아니야!" 하시는 분들이 계시겠죠... 그렇습니다. UTF-8로 인코딩이 되지요.

그럼 확인해보겠습니다. 아주 간단한 실험이죠.

xaml코드에 이미지 object하나를 집어넣고.

한글 파일 경로의 소스를 집어넣습니다. 그리고 바로 breakPoint를 걸어서 파일 경로를 체크해보죠..

결과는 다음과 같죠.

사용자 삽입 이미지


잘 안보이시겠지만.. 뒤쪽에 글자가 UTF8로 URL인코딩되어서

"프로그레스참조.png" 란 이름이.

"%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8A%A4%EC%B0%B8%EC%A1%B0.png"

이렇게 바뀌었습니다.

그럼 밖에서는 어떻게 호출해주고 있는지 살펴볼까요..

Fiddler를 통해 살펴보면 다음과 같은 주소를 호출함을 알 수 있습니다.

사용자 삽입 이미지

경로를 확인해보면 다음과 같습니다.

http://www.btxkorea.com/%C7%C1%B7%CE%B1%D7%B7%B9%BD%BA%C2%FC%C1%B6.png

위의 경로를 IE 주소창에 입력하면 원하는 이미지를 잘 불러오는 것을 볼 수 있습니다.

그런데... 살펴보면 인코딩을 위에 UTF-8과는 다른 인코딩을 쓴다는 것을 알 수 있습니다.

바로 euc-kr이죠.... 왜 이런 인코딩이 되는 건지 알 수 없지만. 일단은 이미지를 잘 불러오므로 pass...


하지만 문제는 FireFox에서 발생합니다. 똑같은 프로젝트를 FireFox에서 실행해보겠습니다.

파폭에서도 똑같은 BreakPoint에서 경로를 확인해보면 UTF-8로 인코딩된 경로를 확인해볼 수 있습니다.

그런데 IE에서 멀쩡히 잘 작동하던 이 코드는 ImageFailed 에러를 날리게 됩니다.

ImageFailed 이벤트에 breakPoint 를 걸어 다시 한번 UriSource 를 확인해봐도

UriSource는 변함이 없음을 확인할 수 있습니다.

  그렇다면 브라우져에서는 어떻게 호출했는지 알아보겠습니다.
IE에서처럼 euc-kr로 호출을 했을까요?

 결과는 다음과 같습니다.

사용자 삽입 이미지

경로를 붙여 넣어 보면 다음과 같습니다.

http://www.btxkorea.com/%C3%AD%C2%94%C2%84%C3%AB%C2%A1%C2%9C%C3%AA%C2%B7%C2%B8%C3%AB%C2%A0%C2%88%C3%AC%C2%8A%C2%A4%C3%AC%C2%B0%C2%B8%C3%AC%C2%A1%C2%B0.png

당연히 이 주소는 IE에서도 FireFox에서도 이미지를 가지고 올 수 없는 경로입니다. 그러면 과연 이 인코딩은

뭘까요? 이미 인코딩이 깨져버린 상태라 되돌릴 수 있는 방법은 없어 보입니다. 거의 난독화 수준이죠...

당연히 이미지도 못가져오고요...



일단 이 버그는 인정하고... 그럼 해결 방법은 무얼까요?

맨처음에는 URL인코딩을 한번 해서 보내는 방법을 써보았습니다.

img.Source = new BitmapImage(new Uri(HttpUtility.UrlEncode("한글경로"),UriKind.RelativeOrAbsolute));

뭐 이런 식이죠.. 결과는 이렇습니다.

http://www.btxkorea.com/%C3%AD%C2%94%C2%84%C3%AB%C2%A1%C2%9C%C3%AA%C2%B7%C2%B8%C3%AB%C2%A0%C2%88%C3%AC%C2%8A%C2%A4%C3%AC%C2%B0%C2%B8%C3%AC%C2%A1%C2%B0.png

정확히 인코딩을 안한것과 결과가 같죠.. 옳거니 실버라이트에서는 무조건 인코딩을 두번해줘야 인코딩이

먹는다는 포스팅을 본적이 있었죠.  그럼 인코딩을 두번해볼까요?

결과는 다음과 같습니다.

다음과 같이 두번 호출이 됨을 볼 수 있습니다.

http://btxkorea.com/%ed%94%84%eb%a1%9c%ea%b7%b8%eb%a0%88%ec%8a%a4%ec%b0%b8%ec%a1%b0.png

http://btxkorea.com/%C7%C1%B7%CE%B1%D7%B7%B9%BD%BA%C2%FC%C1%B6.png

UTF-8로 호출된 주소가 euc-kr로 자동으로 변환되어 호출되고 있죠.

ie에서도 잘 작동함을 알 수 있습니다.


그럼 한가지 실험을 더 해보겠습니다. 다음은 aspx 사이트에서 주소를 redirect해야 하는 경우입니다.

저같은 경우에는 그냥 실버라이트의 Default.aspx 사이트를 이용했습니다.

여기서 value로 한글화일명을 받아 이미지를 redirect해보도록 하겠습니다.

대략적인 코드는 다음과 같죠.

            if (Request.QueryString["value"] != null)
            {
                Response.Redirect("http://www.btxkorea.com/" + Request.QueryString["value"]);
            }

간단하죠...

실버라이트내 코드는 일단 다음과 같습니다.

 img.Source = new BitmapImage(new Uri("http://localhost:56689/Default.aspx?value=프로그레스참조.png", UriKind.Absolute));

그럼 실행시켜보죠...

호출주소는 역시

http://localhost:56689/Default.aspx?value=%C3%AD%C2%94%C2%84%C3%AB%C2%A1%C2%9C%C3%AA%C2%B7%C2%B8%C3%AB%C2%A0%C2%88%C3%AC%C2%8A%C2%A4%C3%AC%C2%B0%C2%B8%C3%AC%C2%A1%C2%B0.png

그럼 다시 인코딩을 두번해 보겠습니다.

그럼 역시 잘 불러오는 것을 확인할 수 있습니다.

그런데 여기도 역시 처음에 UTF-8로 인코딩된 주소를 호출하고 그 뒤에 euc-kr로 된 주소를 다시

호출하는 것을 볼 수 있습니다.

그럼 UTF-8을 해석할 수 없는 서버나. 화일이름을 그대로 받아 euc-kr로 변환해서 redirect 해주는

asp의 경우는 어떻게 해야 할까요...

실제로 이런 사태가 C모월드 프로젝트 도중에 발생하였습니다. 한글이미지 경로는 euc-kr로 변환되어

경로가 저장되어 있고... 중간에 redirect 해주는 asp 페이지는 euc-kr로만 우직하게 request를 변환해줬죠..

그래서 실버라이트에서 utf-8로 인코딩되어 날라간 주소는 이상한 암호문이 될 수 밖에 없었습니다 .
 
 일단 이런 문제가 발생할 가능성은 생각보다 적을 것같지만 다음의 경우 제한이 생길 수 밖에 없을 것같습니다.

ASP 나 ASP.net 을 쓰는 서버의 경우, 서버의 인코딩이 euc-kr로 설정되어 있는 경우... 특별히 web.config

파일의 다음부분이

    <system.web>
    <globalization requestEncoding="euc-kr" responseEncoding="euc-kr" />

위와 같이 설정되어 있는 경우 Request 값을 무조건 euc-kr로 decoding 하게 되죠..

이 경우..(물론 간단히 utf-8로 하게 바꾸면 되겠지만.. 기존 서비스와의 호환성 문제때문에 불가한 경우도 있죠.)..

정말 방법이 없더군요... 그래서 결국 생각해 낸 방법은 UrlEncoding이 되지 않도록 하자는 것이었어요..

결국 방법은 다음과 같죠.
 
UTF-8로 인코딩한 것을 base64로 인코딩해서 request 파라미터 값으로 보내는 것입니다.

  클라이언트에서 값을 보낼때는 다음과 같이 보냅니다.

img.Source = new BitmapImage(new Uri("http://localhost:56689/Default.aspx?value=" + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("프로그레스참조.png")), UriKind.Absolute));

UTF-8로 인코딩한 것을 base64로 인코딩해서 request 파라미터 값으로 보내는 것이죠.. 이러면 중간에

인코딩 과정이 안들어가기 때문에 온전히 서버에서 request value값을 얻을 수 있죠.

그리고 다시 서버에서는 다음과 같이 redirect해주면 됩니다.

            if (Request.QueryString["value"] != null)
            {
                Response.Redirect("http://www.btxkorea.com/" +   System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Request.QueryString["value"])));
            }

뭐 반대로 인코딩을 해주는 것이죠...

이러면.. 온전한 한글 value 값을 euc-kr 을 지원하는 asp 서버에서도 받을 수 있게 되죠.^^;;

그런데... 두둥...!!!!!;;;

여기서 문제가 사라지진 않았습니다. Base64 인코딩을 하다보면..

빈칸의 경우 '+'  로 표시 되죠. 그런데 이것이 이상하게도 서버쪽으로 넘기면 그냥 빈칸으로

넘어오게 되어서 디코딩시 잘못된 길이라는 에러가 뜹니다. 그래서 받은 Request를 한번 더

처리해주어야 합니다.

결론적으로.. 다음과 같겠죠.

            if (Request.QueryString["value"] != null)
            {
                Response.Redirect("http://www.btxkorea.com/"
                  +   System.Text.Encoding.UTF8.GetString(
                              Convert.FromBase64String(Request.QueryString["value"].Replace(" ","+")));

            }

간단히 정리하자면 다음과 같습니다.

1. 한글 url의 인코딩이 파폭과 ie가 다르다.

2. 기본적으로 Image의 경우에도 화일이름은 두번 인코딩해서 사용하자.

3. 서버에서 redirect해주는 경우 서버는 반드시 UTF-8로 설정되어 있어야 한다.

4. 서버설정이 euc-kr일 경우에는 삽질을 좀 많이 해야 한다.

이상과 같네요...--;;;

  모두들 삽질 덜하시고 코딩하시길...^^;;

                                                                                                                     - smile -




Trackback 0 Comment 0
2008/08/18 10:24

여러개의 VisualState 조작시 오작동




위의 프로젝트는 여러객체의 VisualState를 동시에 조작할 경우의 오작동을 테스트한 프로젝트입니다.

실험 방법은 간단합니다. 프로젝트를 실행하면 다음과 같은 화면이 나옵니다.


사용자 삽입 이미지


왼쪽에는 작은 직사각형 모양의 UserControl 이 StackPanel 안에 담겨 있고 현재 상태는 Normal 상태로

그냥 빨간색으로만 보이게 해두었습니다. 여기서 마우스 over를 하면 다음과 같이 변합니다.

사용자 삽입 이미지


여기서 왼쪽 패널을 적당히 훑어서 MouseOver 상태가 제대로 작동하는 것을 확인합니다.

이후에 오른쪽에 All Over State 버튼을 눌러봅니다.

   All Over State 를 누르면 VisualState를 직접 조정해주어서 왼쪽에 모든 UserControl을

Over상태로 바꾸어 줍니다.  그러면 오른쪽에 있는 모든 UserControl이 회색빛으로 바뀌어야

겠죠?.. 그런데 결과는 이렇습니다.

 
사용자 삽입 이미지


아주 랜덤하게 중간에 하나씩 이 빠진 놈이 생기게 됩니다.

왼쪽 리스트에 갯수가 적으면 경우의 수가 줄어들지만 많으면 많아질 수록 확율이 증가하는 것 같습니다.


반대의 경우도 생기는데 위의 상태에서 적당히 몇개의 Rectangle만 MouseOver후 빠져나와 다시 빨간색으로

바꾸어 줍니다. 그 후 All Leave State 버튼을 누릅니다. 이 역시 모두 빨간색으로 변해야만 하겠지만 결과는

위의 경우와 똑같이 랜덤하게 한 두 개씩 상태가 변하지 않는 경우가 생깁니다.

 

  두가지 상황 모두 어떠한 애러도 발생하지 않습니다. 이런 경우가 특히 문제가 되는 경우가 바로 MultiSelect

지원 ListBox를 만들었을 때입니다. 이 경우 어쩔 수 없이 여러개의 ListBoxItem의 상태를 동시에 조정해주어야

하는데 이런 경우 위와 같은 애러가 랜덤하게 일어나게 됩니다. 물론 Visual 적으로만 영향을 줄뿐 코드상으로

어떤 문제도 발생하지 않습니다.

  그럼 모두 한번 테스트 해보시고 feedback 부탁드립니다.

  그리고 테스트 방법등이나 이와 관련된 사항에 대해 질문이 있으신 분들도 언제든지 댓글을 남겨주시길

바랍니다.


                                                                                                                   - smile -

  
Trackback 0 Comment 0
2008/08/14 11:26

(Firefox에서) 실버라이트 런타임 또 깔으라고 나오는 경우 중 하나!

원문 : 길버라이트 (http://gilverlight.net/2917)

조금 황당한 경우입니다.
이런 문제를 마주치시더라도 당황하지 마세요.

아시는 바와 같이 Visual Studio에서 실버라이트 프로젝트를 생성하면
*.aspx 와 *.html 견본페이지가 생성됩니다.

특히 *.html 페이지에 보면 object를 사용하여 실버라이트를 호스팅하는
부분이 있습니다.

보통 아래와 같습니다.

<object data="data:application/x-silverlight," type="application/x-silverlight-2-b2" width="100%" height="100%">
   <param name="source" value="ClientBin/ZoomPanningContainerSample.xap"/>
   <param name="onerror" value="onSilverlightError" />
   <param name="background" value="white" />
   
   <a href="http://go.microsoft.com/fwlink/?LinkID=115261" style="text-decoration: none;">
        <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/>
   </a>
  </object>

첫줄에 제가 빨간 색으로 표시한 부분이 보이시나요?
object태그의 data 속성이 data:application/x-silverlight,입니다.

끝에 ,(comma)가 있습니다. 이거 함부로 없애시면 안됩니다. ^^;;;

IE에서는 문제 없습니다. 하지만 Firefox에서는 저 comma 함부로 떼면,
실버라이트 런타임을 또 깔으라고 하네요.

이.상.하.죠? ^^


결론

실버라이트를 위한 object 태그 data 속성인 data:application/x-silverlight,에서
(당분간은 말이죠.) 마지막의 ,(comma)를 함부로 제거하지 맙시다.

Trackback 0 Comment 0
2008/08/02 14:23

[공지] Error 1001 이란?

Error 1001.

자세히 보기

Trackback 0 Comment 5
2008/07/15 19:18

[강좌] ImageButton 만들기.

보통 버튼을 만들 때 이미지 두장 혹은 이미지 3장만을 가지고 Opacity를  조정해서
만들 때가 많았죠. 그래서 간단하게 ImageButton 을 만드는 방법을 알려드리려고 해요.

  이번 강좌는 너무너무 쉬워서 뭐 이런 걸 강좌라고 썼냐 하는 생각이 들지도 모르겠네요.

암튼 그래서 일단 빠르게 강좌를 진행하도록 하겠습니다.

  일단 버튼 이미지 세장을 준비하겠습니다.

  하나는 보통 상태의 이미지이고 또 하나는 MouseOver시의 이미지 그리고

나머지 하나는 Pressed 되었을 때의 이미지입니다. 뭐 Disabled 되었을 때의 이미지도

준비하고 싶으신분 들은 준비하셔도 됩니다.


 먼저 그냥 Button에 Style을 변경하여 만들어 보도록 하겠습니다.

일단 블랜드에서 Button 하나를 만듭니다.

사용자 삽입 이미지


 그 다음에는 그림의 오른쪽 위쪽에 있는 버튼을 눌러서 새로 스타일을 만듭니다.

사용자 삽입 이미지


Create Empty로 만들고 나면 다음과 같이 물어 봅니다.

사용자 삽입 이미지

    다음과 같이 Style을 새로 만들면

  아무것도 없고 Grid만 덜렁 하나 생겨 있을 것입니다.

그럼 그 안에 Image를 세장 넣고. 적절한 이름을 줍니다.

뭐 'NormalImage', 'OverImage', 'PressedImage'  정도가 되겠죠.

여기까지 끝냈으면 요렇게 나옵니다.
사용자 삽입 이미지

 크기에 따라 적당히 변할 수 있도록 Image의 Width,Height Property 값을 웬만하면 Auto로
주고 Stretch 값도 Uniform 이나 Fill 로 해둡니다.


  자 이제 State 를 줘 보도록 합시다.

  위쪽에 States 패널을 건드려 봅시다.  다음과 같이 있을 텐데요.

사용자 삽입 이미지

일단 기본 상태를 Normal로 두기 위해서 Normal Image를 제외한 다른 이미지의
Visibility를 Collapse로 해둡니다.

위 패널에서 MouseOver 를 클릭한 후에 OverImage를 제외한 나머지 이미지의
 Visibility를 Collapse로 해둡니다.

같은 식으로 Pressed를 클릭한 후에 PressedImage를 제외한 나머지 이미지의
 Visibility를 Collapse로 해둡니다.

쉽죠?^^

자 이제 돌아와서 실행을 시켜보죠.

여기까지의 소스코드는 다음과 같습니다.

Page.xaml
<UserControl x:Class="MakeImageButton.Page"
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <Button Template="{StaticResource ImageButton}" Width="100" Height="50"/>
    </Grid>
</UserControl>

App.xaml
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MakeImageButton.App"
             xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
             >
    <Application.Resources>
       
     <ControlTemplate x:Key="ImageButton" TargetType="Button">
      <Grid Background="#00000000">
       <vsm:VisualStateManager.VisualStateGroups>
        <vsm:VisualStateGroup x:Name="FocusStates">
         <vsm:VisualState x:Name="Unfocused"/>
         <vsm:VisualState x:Name="Focused"/>
        </vsm:VisualStateGroup>
        <vsm:VisualStateGroup x:Name="CommonStates">
         <vsm:VisualStateGroup.Transitions>
         </vsm:VisualStateGroup.Transitions>
         <vsm:VisualState x:Name="MouseOver">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="NormalImage" Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="OverImage" Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </vsm:VisualState>
         <vsm:VisualState x:Name="Pressed">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="NormalImage" Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PressedImage" Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="00:00:00">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </vsm:VisualState>
         <vsm:VisualState x:Name="Disabled">
          <Storyboard/>
         </vsm:VisualState>
         <vsm:VisualState x:Name="Normal">
          <Storyboard/>
         </vsm:VisualState>
        </vsm:VisualStateGroup>
       </vsm:VisualStateManager.VisualStateGroups>
       <Image Height="Auto" HorizontalAlignment="Right" x:Name="NormalImage" VerticalAlignment="Bottom" Width="Auto" Source="Normal.png" Stretch="Fill"/>
       <Image Height="Auto" HorizontalAlignment="Right" x:Name="OverImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Over.png" Stretch="Fill"/>
       <Image Height="Auto" HorizontalAlignment="Right" x:Name="PressedImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Pressed.png" Stretch="Fill"/>
      </Grid>
     </ControlTemplate>
       
    </Application.Resources>
</Application>

어려울 것은 없죠.. 실제로는 다음과 같이 작동하겠죠.



그런데.. 아무리 블랜드에서 작업하는게 편하다고 해도 이미지로 버튼을 만들 때 마다

이렇게 똑같은 작업을 반복한다고 하면 정말 지겨운 일이 아닐 수 없을 겁니다.

그냥 아래와 같이 프로퍼티만 셋팅하면 자동으로 이미지 3장으로 버튼을 만들 수 있으면

좋겠죠?

<ImageButton NormalImageSource="Normal.png" OverImageSource="Over.png" PressedImageSource="Pressed.png" />

  그래서 이제부터는 이렇게 간단하게 만들어 쓸 수 있는 ImageButton을 만들려고 합니다.

일단 기본적으로는 버튼하고 똑같고 위의 Style을 그대로 따르지만 Image의 Source만 다른 프로퍼티로 바꿀 수 있었으면 좋겠죠.

 예전의 짱묜님의 강좌에서는 이미지가 한장뿐이어서 Tag를 사용해서 해결했지만 이경우는
3개나 되니.. 그 방법은 쓰기가 힘들죠.

  그럼 일단 ImageButton을 만들어 보겠습니다. 간단히 class 를 하나 만드십쇼.

ImageButton이란 이름으로 말이죠. 그리고 Button을 상속 받습니다.

    public class ImageButton : Button
    {}


테스트 해보시면 알겠지만 이상태로도 바로 Button처럼 작동하게 됩니다.

Page.xaml
<UserControl x:Class="MakeImageButton.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:MakeImageButton;assembly=MakeImageButton"
    Width="135" Height="37">
    <Grid x:Name="LayoutRoot">  
        <my:ImageButton>    
    </Grid>
</UserControl>

이렇게만 해보아도(물론 자기 클래스를 my란 이름의 네임스페이스로 선언했을 때죠.) Button 과 똑같이 작동한다는 것을 알 수 있을 것입니다.

  이제 우리가 원하는 프로퍼티를 추가해야 할 때입니다.

  일단 위에서 언급한 NormalImageSource, OverImageSource, PressedImageSource 등은
xaml에서 다룰 수 있어야 하고 다른 Template의 값들과 Binding 이 가능하여야 하죠.

 이런 조건을 충족시켜주기 위해서는 DependencyProperty로 등록을 하여야만 합니다.

여기 쉽게 DependencyProperty를 추가해줄 수 있는 snippet이 있습니다.


유용하게 쓰시고 그럼 이걸 이용해서

프로퍼티 3가지를 추가 합니다. 여기서  Property의 Type은 ImageSource로 해줍니다.

그럼 다음과 같은 코드가 추가 될 것입니다.

        #region NormalImageSource
        /// <summary>
        /// Gets or sets the NormalImageSource possible Value of the Uri object.
        /// </summary>
        public ImageSource NormalImageSource
        {
            get { return (ImageSource)GetValue(NormalImageSourceProperty); }
            set { SetValue(NormalImageSourceProperty, value); }
        }
        /// <summary>
        /// Identifies the NormalImageSource dependency property.
        /// </summary>
        public static readonly DependencyProperty NormalImageSourceProperty =
                    DependencyProperty.Register(
                          "NormalImageSource",
                          typeof(ImageSource),
                          typeof(ImageButton),
                           new PropertyMetadata(OnNormalImageSourcePropertyChanged));
        /// <summary>
        /// NormalImageSourceProperty property changed handler.
        /// </summary>
        /// <param name="d">ImageButton that changed its NormalImageSource.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs.</param>
        private static void OnNormalImageSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ImageButton _ImageButton = d as ImageButton;
            if (_ImageButton != null)
            {
                
            }
        }
        #endregion NormalImageSource
        #region OverImageSource
        /// <summary>
        /// Gets or sets the OverImageSource possible Value of the Uri object.
        /// </summary>
        public ImageSource OverImageSource
        {
            get { return (ImageSource)GetValue(OverImageSourceProperty); }
            set { SetValue(OverImageSourceProperty, value); }
        }
        /// <summary>
        /// Identifies the OverImageSource dependency property.
        /// </summary>
        public static readonly DependencyProperty OverImageSourceProperty =
                    DependencyProperty.Register(
                          "OverImageSource",
                          typeof(ImageSource),
                          typeof(ImageButton),
                           new PropertyMetadata(OnOverImageSourcePropertyChanged));
        /// <summary>
        /// OverImageSourceProperty property changed handler.
        /// </summary>
        /// <param name="d">ImageButton that changed its OverImageSource.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs.</param>
        private static void OnOverImageSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ImageButton _ImageButton = d as ImageButton;
            if (_ImageButton != null)
            {
                //TODO: Handle new value.
            }
        }
        #endregion OverImageSource
        #region PressedImageSource
        /// <summary>
        /// Gets or sets the PressedImageSource possible Value of the Uri object.
        /// </summary>
        public ImageSource PressedImageSource
        {
            get { return (ImageSource)GetValue(PressedImageSourceProperty); }
            set { SetValue(PressedImageSourceProperty, value); }
        }
        /// <summary>
        /// Identifies the PressedImageSource dependency property.
        /// </summary>
        public static readonly DependencyProperty PressedImageSourceProperty =
                    DependencyProperty.Register(
                          "PressedImageSource",
                          typeof(ImageSource),
                          typeof(ImageButton),
                           new PropertyMetadata(OnPressedImageSourcePropertyChanged));
        /// <summary>
        /// PressedImageSourceProperty property changed handler.
        /// </summary>
        /// <param name="d">ImageButton that changed its PressedImageSource.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs.</param>
        private static void OnPressedImageSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ImageButton _ImageButton = d as ImageButton;
            if (_ImageButton != null)
            {
               
            }
        }
        #endregion PressedImageSource

On어쩌구PropertyChanged 함수는 null 값을 넣어주고 구지 구현하지 않아도 상관없는 부분입니다.

  그러면 이제 ImageButton에 맞는 Default Style을 준비할 차례입니다.

먼저 xml 화일이나 cs화일이 없는 xaml 화일을 만듭니다. 그리고 이름을 generic.xaml

로 줍니다. 반드시 ImageButton 이 있는 프로젝트의 루트 부분에 있어야 합니다.

  그리고 generic.xaml 의 Build Action 을 Resource로 해주어야 합니다.

  자 그러면 이제 내용을 살펴 볼까요.

<ResourceDictionary
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
     xmlns:my="clr-namespace:MakeImageButton;assembly=MakeImageButton">
</ResourceDictionary>

요정도가 될 것같습니다.

여기에 Button의 기본적인 프로퍼티값을 setter로 셋팅해주고
Template 부분은 요전에 만들어 놨던 Button Template 값을 그대로 대체시켜줍니다.
거기에 Key값은 없애고 TargetType="my:ImageButton" 로 둡니다.

여기까지 잘 따라오셨다면 다음과 같은 코드가 생깁니다.
 <!--ImageButton-->
 <Style TargetType="my:ImageButton">
  <Setter Property="IsEnabled" Value="true" />
  <Setter Property="IsTabStop" Value="true" />
  <Setter Property="Background" Value="#00000000" />
  <Setter Property="Foreground" Value="#FF313131" />
  <Setter Property="MinWidth" Value="5" />
  <Setter Property="MinHeight" Value="5" />
  <Setter Property="Margin" Value="0" />
  <Setter Property="HorizontalContentAlignment" Value="Center" />
  <Setter Property="VerticalContentAlignment" Value="Center" />
  <Setter Property="Cursor" Value="Arrow" />
  <Setter Property="TextAlignment" Value="Left" />
  <Setter Property="TextWrapping" Value="NoWrap" />
  <Setter Property="FontSize" Value="11" />
  <Setter Property="Template">
   <Setter.Value>
                <ControlTemplate TargetType="my:ImageButton">
                    <Grid Background="#00000000">
                        <vsm:VisualStateManager.VisualStateGroups>
                            <vsm:VisualStateGroup x:Name="FocusStates">
                                <vsm:VisualState x:Name="Unfocused"/>
                                <vsm:VisualState x:Name="Focused"/>
                            </vsm:VisualStateGroup>
                            <vsm:VisualStateGroup x:Name="CommonStates">
                                <vsm:VisualStateGroup.Transitions>
                                </vsm:VisualStateGroup.Transitions>
                                <vsm:VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="NormalImage" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="OverImage" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Pressed">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="NormalImage" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PressedImage" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Disabled">
                                    <Storyboard/>
                                </vsm:VisualState>
                                <vsm:VisualState x:Name="Normal">
                                    <Storyboard/>
                                </vsm:VisualState>
                            </vsm:VisualStateGroup>
                        </vsm:VisualStateManager.VisualStateGroups>
                        <Image Height="Auto" HorizontalAlignment="Right" x:Name="NormalImage" VerticalAlignment="Bottom" Width="Auto" Source="Normal.png" Stretch="Fill"/>
                        <Image Height="Auto" HorizontalAlignment="Right" x:Name="OverImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Over.png" Stretch="Fill"/>
                        <Image Height="Auto" HorizontalAlignment="Right" x:Name="PressedImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Pressed.png" Stretch="Fill"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
  </Setter>
 </Style>

이제 다시 ImageButton.cs 로 돌아와서 생성자를 만들어줍니다.

        public ImageButton()
        {
              DefaultStyleKey = typeof(ImageButton);
        }

여기까지만 해도 일단 집어넣은 이미지가 있으니 작동을 하지요.

그런데 여기서 우리가 원하는 것은 프로퍼티 값을 바꾸어서 얼마든지 Image를 바꿀 수 있게
하는 것이죠. 자 그럼 Template 부분을 이렇게 바꾸어 봅시다.

Image Height="Auto" HorizontalAlignment="Right" x:Name="NormalImage" VerticalAlignment="Bottom" Width="Auto" Source="Normal.png" Stretch="Fill"/>
                        <Image Height="Auto" HorizontalAlignment="Right" x:Name="OverImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Over.png" Stretch="Fill"/>
                        <Image Height="Auto" HorizontalAlignment="Right" x:Name="PressedImage" VerticalAlignment="Bottom" Width="Auto" Visibility="Collapsed" Source="Pressed.png" Stretch="Fill"/>


                                                            ↓
<Image x:Name="NormalImage" Source="{TemplateBinding NormalImageSource}" Stretch="Fill"/>
<Image x:Name="OverImage" Source="{TemplateBinding OverImageSource}" Stretch="Fill" Visibility="Collapsed"/>
<Image x:Name="PressedImage" Source="{TemplateBinding PressedImageSource}" Stretch="Fill" Visibility="Collapsed"/>

   
  쓸데없는 프로퍼티들은 일부러 제거 했습니다.

그리고 이제 우리가 애초에 원하던 대로 코드를 바꾸어 봅시다.

Page.xaml
<UserControl x:Class="MakeImageButton.Page"
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="clr-namespace:MakeImageButton;assembly=MakeImageButton"
    Width="135" Height="37">
    <Grid x:Name="LayoutRoot">  
        <my:ImageButton NormalImageSource="Normal.png" OverImageSource="Over.png" PressedImageSource="Pressed.png">    
    </Grid>
</UserControl>

잘 작동하시나요?

Stretch 등의 속성도 추가하여 Image Stretch 형태도 바꾸어 줄 수 있습니다.

소스코드는 아래에 첨부합니다.


압축을 푼 뒤 한번 빌드 후에 실행하시면 될겁니다.

주의하셔할 것 몇가지를 언급하자면 ImageSource는 절대 두번이상 셋팅 될 수 없습니다.

만약 두 번 이상 셋팅 해주시려고 한다면 Property의 Get 함수에서 BitmapImage 을 새로

생성해서 반환해주는 코드를 짜주어야 합니다.

        public ImageSource NormalImageSource
        {
            get { return new BitmapImage(new Uri((GetValue(NormalImageSourceProperty) as BitmapImage).UriSource.OriginalString, UriKind.RelativeOrAbsolute)); }
            set { SetValue(NormalImageSourceProperty, value); }
        }

  뭐 이정도 소스가 되겠죠..^^;;

 
  이렇게 글로 설명하는데는 별로 익숙하지 못해서 잘 이해가 되셨는지 모르겠군요.

자 그럼 이제 Custom Control의 세계로...^^~~~

                                                                                          - smile -
Trackback 0 Comment 0