'실버라이트'에 해당되는 글 20건

  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. 2008.12.13 [강좌] ListBox의 상속. 1
  7. 2008.12.10 [강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.
  8. 2008.12.10 [강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.
  9. 2008.12.09 [강좌] ContentControl?? ContentPresenter 1
  10. 2008.12.02 ListBox의 Select된 객체 해제하기.(Select취소하기) 1
  11. 2008.11.25 FontSource 설정시 주의!!! 1
  12. 2008.10.10 RepeatedButton 의 Style이름 필수 요소 2
  13. 2008.09.29 팁 : 실버라이트 2 RC0 포팅 시 Style에서 발생하는 오류
  14. 2008.09.19 VisualState 의 동적 제어.
  15. 2008.08.18 여러개의 VisualState 조작시 오작동
  16. 2008.08.14 (Firefox에서) 실버라이트 런타임 또 깔으라고 나오는 경우 중 하나!
  17. 2008.07.09 UserControl 부모, 자식이 같은 이름의 스토리보드를 가지고 있을 경우, '간혹' 발생하는 에러
  18. 2008.07.04 ColorAnimation 의 작동 오류
  19. 2008.07.04 [Beta1->Beta2] 블렌드에서 An Exception was thrown 에러 발생 시!
  20. 2008.06.21 [삽질을덜하자!] FullScreenChanged Event 너무 믿지 마세요.
2009. 6. 4. 21:58

Collection 바인딩과 InvalidOperationException

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

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

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



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

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

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

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

수술전
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.}


수술후
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로 초기화해주면
이런 일이 발생하지 않습니다. 정말 바인딩에서 발생하는 문제들은 타이밍에 관련된 것도 있고 오묘해서 해결하기가
쉽지 않은데 노련한 공도씨가 단박에 해결해 주었네요.

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


2009. 5. 6. 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 보기가 부담스럽지 않다!

감사합니다.

2009. 4. 8. 14:48

AttachedProperty와 데이터 바인딩

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

...근데 왜 {Binding Source={StaticResource Source}, Path=Property}가 안먹을까... 이건 의문.
2009. 1. 9. 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 -




2009. 1. 9. 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 -

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 -

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. 역시 샘플 프로젝트를 첨부합니다.


 
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 -




 


 

 
2008. 12. 9. 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. 샘플프로젝트를 첨부합니다. 


2008. 12. 2. 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 -
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 -
2008. 10. 10. 10:11

RepeatedButton 의 Style이름 필수 요소


바로 메인 루트의 이름을 "Root" 라고 꼭 이름을 넣어주어야 Repeated 버튼을 제대로 이용할 수 있습니다.

이름을 주지 않을 경우 Repeated가 더이상 Repeated가 되지 않더군요.^^

그럼 모두들 주의!!!

                                                                                                   - smile -
2008. 9. 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 한다.

자, 이제 프로그램이 잘 뜬다.
2008. 9. 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도 동적 제어가 가능합니다.

2008. 8. 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 -

  
2008. 8. 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)를 함부로 제거하지 맙시다.

2008. 7. 9. 16:36

UserControl 부모, 자식이 같은 이름의 스토리보드를 가지고 있을 경우, '간혹' 발생하는 에러

상황설명

UserControl 부모와 UserControl 자식이
UserControl.Resource 하위에 같은 이름을 가진 스토리보드를 가지고 있는 상황에서,
자식을 생성하여 부모의 LayoutRoot.Children에 Add를 하려고 시도하면,
경우에 따라서 다음 그림과 같이 ArgumentException을 유발합니다.

사용자 삽입 이미지

정확히 어떤 경우에 이렇게 되는지 실험을 통해 알아 보았습니다.


실험조건

Fafamama 클래스 - 부모 역할을 하는 UserControl 입니다.
Son 클래스 - 자식 역할을 하는 UserControl입니다.
Daughter 클래스 - 자식 역할을 하는 UserControl입니다.

Fafamama는 LayoutRoot의 Opacity를 조정하는 sbShow란 Storyboard를 UserControl.Resources 하위에 가지고 있으며,
Son은 Fafamama를 쏙 빼 닮아 자신의 LayoutRoot의 Opacity를 조정하는 sbShow란 똑같은 이름의
Storyboard를 가지고 있습니다
.
Daughter는 Son이 가지고 있는 스토리보드와 똑같은 스토리보드를 이름만 다르게 하여 가지고 있습니다.

Fafamama.MakeSons() 메서드 - 두 명의 아들을 생성합니다.
Fafamama.MakeDaughters() 메서드 - 두 명의 딸을 생성합니다.

총 6가지 케이스를 실험하기 위해 6번 Fafamama를 생성합니다.

Case 1) Fafamama의 생성자에서 MakeDaughters 호출
Case 2) Fafamama의 Loaded 이벤트에서 MakeDaughters 호출
Case 3) 버튼 클릭에 의해 이미 생성되어 있는 Fafamama의 MakeDaughters 호출

Case 4) Fafamama의 생성자에서 MakeSons 호출
Case 5) Fafamama의 Loaded 이벤트에서 MakeSons 호출
Case 6) 버튼 클릭에 의해 이미 생성되어 있는 Fafamama의 MakeSons 호출


라이브 실험

아래 실버라이트 어플리케이션 안에 실험 Case 6개에 대한 버튼을 준비 하였습니다.
한번 씩 눌러보십시오.

Get Microsoft Silverlight


실험결과

부모와 같은 이름의 Storyboard를 가지고 있지 않은 Daughter의 경우, 언제나 예외발생 없이 생성이 된 반면,
UserControl 부모, 자식이 같은 이름의 스토리보드를 가지고 있는 Son의 경우,
생성자에서 호출 시를 제외하고서는 부모의 Panel에 Children.Add 할 수 없었습니다.
이런 경우를 만나신다면 당분간은 피해서(스토리보드 이름을 중복되지 않게 준다거나, 이름을 유지하고, 다른 객체의 Resources 하위로 옮김) 사용해야 하겠습니다.

다운로드


실험에 사용된 프로젝트입니다.



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

2008. 7. 4. 11:19

ColorAnimation 의 작동 오류

VisualStateManger로 State를 관리하면서,

여러 개의 객체를 동시에 상태를 변경하는 경우!

객체 중 몇 개의 Storyboard가 Freezing하는 현상이 있습니다.

다른 Animation의 경우는 정상 작동하지만 ColorAnimation만

중간에 Freezing 되어 작동이 되지 않습니다.

그러다가 윈도우 창을 조금만 조정하여 redraw가 되게 하면

부분적으로 정상 작동하게 됩니다.


아래 예제를 첨부하니 참고 해보길 바랍니다.


버그를 발생시키는 루틴은.. 맨처음 오른쪽에 마우스 오버를 시켜 State 변화를 몇번

시킵니다. 그 후에 오른쪽 버튼을 눌러 전체의 State를 한꺼번에 바꿉니다.

 한두 번 하다보면 이가 빠진 것처럼 작동을 안 하는 객체들이 보일 겁니다.

피해가는 방법은 ColorAnimation을 안 쓰고 다른 에니메이션을 이용하는 것이죠.
Opacity조정이나 Visibility조정 등...

예제처럼 여러 개의 객체를 동시에 Animation을 주어야 할 경우에는 되도록
ColorAnimation을 피해야 할 것 같습니다. ^^;;

 

2008. 7. 4. 09:38

[Beta1->Beta2] 블렌드에서 An Exception was thrown 에러 발생 시!

Beta1에서 잘 작동했던 프로젝트,
Beta2에서 블렌드로 열었을 때 이런 화면 보신 적 있으시죠?

사용자 삽입 이미지


당황하실 필요없습니다.
UserControl에 대한 XML의 namespace가 변경되어서 그런 것 뿐이예요.

자, XAML 코드뷰로 가셔서 UserControl의 Attribute를 다음과 같이 변경하세요.

수술 전
<UserControl x:Class="HugeProject.Gilbert"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" ...>

수술 후
<UserControl x:Class="HugeProject.Gilbert"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

이렇게 하시면 Blend에서 미리보기 아주 자~알 나옵니다.

감사합니다.
2008. 6. 21. 11:18

[삽질을덜하자!] FullScreenChanged Event 너무 믿지 마세요.

개요

오늘은 Application.Current.Host.Content.FullScreenChanged 이벤트에 대해서
한 말씀드리려고 합니다.

실버라이트 2 버전에서부터 Grid 등의 등장과 함께 화면구성하기가 용이해 졌습니다.
예를들어 LayoutRoot의 Width, Height를 Auto로 설정하고,
HorizontalAlignment, VerticalAlignment를 Stretch로 설정하고,
Margin 적당히 주면, 어떤 화면 사이즈에서도 척척 알아서 변하는 UI로 꾸밀 수 있죠.

FullScreen Mode에서는 어떨까요?
네! 화면이 FullScreen이 되면 위에 언급한 것과 같이 구성된 UI는
전체화면 사이즈에 맞게 변화합니다.

하지만 여기에 시차가 존재합니다.

FullScreenChanged 이벤트 핸들러 함수에서 어떤 UIElement의 ActualWidth, ActualHeight
등 사이즈에 관련된 프로퍼티를 참조하는 것은 바람직하지 않습니다.

그 이유를 실험을 통해 알려드리겠습니다.

다운로드 : 실험을 위한 샘플 프로젝트


실험방법

사용자 삽입 이미지

50ms의 Interval로 설정된 DispatcherTimer가
ScrollViewer와 LayoutRoot, Application.Current.Host.Content의 Width를 출력합니다.

FullScreenChanged 이벤트 핸들러에서, [IsFullScreen = true / false]를 출력합니다.

샘플 어플리케이션이 실행되면, FullScreen On/Off 버튼을 눌러 전체화면으로 갔다가,
ESC 키를 눌러 전체화면을 해제한 다음 Stop Timer를 눌러 타이머에 의한 출력을 중단합니다.


실험결과

사용자 삽입 이미지
실험 결과를 보면 'FullScreen Mode가 전환되는 시점'에서
Application.Current.Host.Context.ActualWidth를 제외한 다른 UIElement의 ActualWidth는
바람직한 사이즈 값을 갖게 되는데 시간 지연이 있음을 알 수 있습니다.


결론

FullScreen 전환에 따라 변화한 UIElement들의 사이즈를 참조하려면,
Application.Current.Host.Content.FullScreenChanged 이벤트 핸들러 보다는

SizeChanged 이벤트 핸들러에서 Application.Current.Host.Content.IsFullScreen의
분기문을 작성하여 구현하는 것이 안전하다.