'silverlight'에 해당되는 글 41건
- 2009/06/04 Collection 바인딩과 InvalidOperationException (1)
- 2009/05/06 Visual Studio에서 XAML을 열 때 [미리보기] 안하기 (1)
- 2009/04/08 AttachedProperty와 데이터 바인딩
- 2009/01/09 실버라이트에서의 UI 자동화 - Part 2 (쉬운 방법)
- 2009/01/09 [번역] 실버라이트에서의 UI Automation - 사용자 인터랙션 시뮬레이션
- 2009/01/05 UI Automation에 관련된 글. (1)
- 2009/01/02 Dependecy Property에 관한 블로그
- 2008/12/29 Silverlight Bugs and Workarounds
- 2008/12/22 Custom Panel 만들기. (1)
- 2008/12/13 [강좌] ListBox의 상속. (1)
- 2008/12/10 [강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.
- 2008/12/10 [강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.
- 2008/12/09 [강좌] ContentControl?? ContentPresenter (1)
- 2008/12/09 [강좌] ListBox 를 사용하자!!
- 2008/12/02 ListBox의 Select된 객체 해제하기.(Select취소하기) (1)
- 2008/11/25 FontSource 설정시 주의!!! (1)
- 2008/11/12 YouCard Re-visited: Implementing the ViewModel pattern 해석
- 2008/11/05 Wheel 지원 리스트 박스
- 2008/10/27 VisualStudio 에서의 미리 보기 에러
- 2008/10/06 Silverlight Unit Test Template (1)
- 2008/09/29 팁 : 실버라이트 2 RC0 포팅 시 Style에서 발생하는 오류
- 2008/09/19 VisualState 의 동적 제어.
- 2008/09/11 실버라이트에서 MD5 암호화
- 2008/08/26 님하 커스텀 커서 지원 점... (1)
- 2008/08/20 ButtonBase의 Space바 클릭.
- 2008/08/18 FireFox에서의 한글로 된 파일 경로.
- 2008/08/18 여러개의 VisualState 조작시 오작동
- 2008/08/14 (Firefox에서) 실버라이트 런타임 또 깔으라고 나오는 경우 중 하나!
- 2008/08/02 [공지] Error 1001 이란? (5)
- 2008/07/15 [강좌] ImageButton 만들기.
실버라이트 어플리케이션을 개발하다보면,
ListBox의 ItemsSource와 List<>나 Collection<>과 같은 IEnumerable류를 바인딩 할 경우가 자주 있습니다.
휴즈플로우에서 진행한 최근 프로젝트 중에서 MVVM 패턴으로 개발한 어플리케이션이 있는데요.
ListBox와 Collection류의 프로퍼티가 바인딩하게 되는 여러 뷰들을 빠른 속도로 전환하다보면,
InvalidOperationException이 발생하였습니다.
Exception에 담겨있는 에러메세지는 "개체의 현재 상태 때문에 작업이 유효하지 않습니다."라는 애매한 메세지였고,
예외가 발생한 곳은 뷰모델의 베이스용으로 구현해 놓은 ViewModelBase의 OnPropertyChanged(...) 함수 내부였습니다.
어플리케이션을 천천히 여유있게 조작하면 문제가 발생하지 않다가, 악의적인 유저로 돌변하여 UI를 이리저리 정신없이
전환시키다 보면 발생하는 Exception인데 해결하기가 여간 어려운게 아니더군요.
이 애매한 문제는 결국 파티션 너머의 공도씨를 호출하여 도움을 받아 해결하였습니다.
수술 전 코드와 수술 후 코드를 보시면서, 어떤 코드가 더 안전한지 이해하실 겁니다.
수술전
01.public List<PHOTO> Photos 02.{ 03. get04. { 05. return _photos; 06. } 07. set08. { 09. _photos = value; 10. 11. OnPropertyChanged("Photos"); 12. } 13.}수술후
01.public List<PHOTO> Photos 02.{ 03. get04. { 05. return _photos; 06. } 07. set08. { 09. if (_photos != null) 10. { 11. _photos.Clear(); 12. _photos = null; 13. } 14. 15. _photos = value; 16. 17. OnPropertyChanged("Photos"); 18. } 19.}콜렉션을 통째로 새 객체로 덮어쓰더라도 전에 사용하고 있던 콜렉션을 Clear해 주고, 변수를 null로 초기화해주면
이런 일이 발생하지 않습니다. 정말 바인딩에서 발생하는 문제들은 타이밍에 관련된 것도 있고 오묘해서 해결하기가
쉽지 않은데 노련한 공도씨가 단박에 해결해 주었네요.
이런 팁은 내용을 이해한 후에 습관화하는 것이 좋을 것 같습니다.
원문 : 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 보기가 부담스럽지 않다!감사합니다.
그래서 AttachedProperty안에 있는 오브젝트에 데이터 바인딩을 걸려면 AttachedProperty에서 Set할 때 안에 들어갈 오브젝트에게 DataContext를 넘겨줘야 함.
<TextBox AutomationProperties.AutomationId="SearchTextBox" x:Name="SearchText" KeyDown="CheckKey" />
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"
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)
};
}
}
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);
}
}
[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");
<appSettings>
<add key="SendKeys" value="SendInput"/>
</appSettings>
UIAutomation에 대한 두번째 번역이 끝났습니다. 사실 UIAutomation은 테스트 자동화보다는 Accessibility에 초점이 맞혀진 기술이죠. 미국의 경우에는 이미 웹페이지에 대한 접근성에 대한 법률이 재정되었다고 하니 앞으로 이런 부분도 신경을 써야 하지 않을까 싶습니다.
프로젝트를 진행하면서 반복되는 리뷸드 그리고 반복되는 타이핑 그리고 반복되는 디버깅을 얼마나 많이 경험했는지 모릅니다. 프로젝트 내부에 아주 조그만한 버그를 잡기 위해 코드를 조금 고치고 프로젝트의 여러 실행단계(로그인 프로세스등등..)를 거쳐서 정말 테스트 하고 싶은 부분을 테스트 한 후 다시 돌아와 디버깅을 해야 하는 경우.. 아무리 앞의 단계를 단순히 한다고해도 정말 짜증나느 일이었죠. 기본적으로 UnitTest화 하여 분리하여 테스트 하는 것이 맞지만 프로젝트를 진행하다보면 그렇게 똑 부러지게 분리되지 않는 경우도 많죠... 그런 경우 이런 테스트 자동화를 통해 어느정도 해결할 수 있지 않을까 합니다.
물론 Unit Test 도 이런 UI 테스트 자동화도 추가적인 코드가 들어가고 자칫 번잡해질 수도 있지만 규모가 큰 프로젝트일 수록 복잡한 프로젝트일 수록 미리미리 이런 것들을 준비해놓는 것이 프로젝트 막판에 패닉상태로 빠지지 않는 지름길이 아닐까 합니다.
Unit Test를 쓰고 안쓰고 UI 테스트 자동화를 하고 안하고는 전적으로 개발자 본인이나 프로젝트 매니저가 선택할 일이지만 일단 사용하려고 하였을 때 제가 번역한 글이 조금이나마 도움이 되길 바랍니다.
그럼.. 마지막으로 제가 이 글을 번역하면서 따라한 테스트 프로젝트를 다시 한번 추가 시켜 놓도록 하겠습니다.
프로젝트를 실행해보는 방법은 먼저 웹프로젝트를 실행시킨뒤 Test를 실행시키시면 됩니다. 그럼..
- smile -
이 글은 다음 링크의 글을 번역한 글입니다.
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 -
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;
}
}
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);
}
}
- 어떤 3x3 블럭에 들어갈 것인가?
- 3x3 블럭안에 어떤 구역 숫자가 들어갈 것인가?
- 3x3 블럭의 Left,Top 코너 위치
- 3x3 블럭안 구역의 Left,Top 코너 위치.
- 각 객체의 DesiredSize.
- 우리가 child.Measure를 호출 했을 때, 우리는 자식들이 가능한 사이즈를 넘겨주는 것입니다. 자식들의 실제 사이즈를 설정해주는 것이 아닙니다.
- Measure 함수를 호출 한 뒤에 Layout System 은 Element의 DesiredSize를 결정할 것입니다. 다시 말해서 제가 말할 수 있는 것은 우리가 직접 DesiredSize를 설정할 수 있는 방법은 없다는 것입니다..
- 우리가 child.Arrange 를 호출 했을 때 우리는 child의 최종 사이즈(final size)를 설정하는 것이 아닙니다. 저는 잠시동안 이것이 child의 사이즈를 설정하는 것이라고 착각했지만 이것은 child를 포함할 경계 영역(bounding box)를 설정하는 것입니다. 그래서 만약 child가 경계영역보다 크다면 그것은 Clip 될 것이며 만약 child가 더 작다면 그것은 기본 정렬(역자주: 보통은 Center 정열)이나 당신이 정해준 정렬에 기반하여 경계 영역안에 위치될 것입니다. 이 Panel 경우에는 경계 영역이 child의 DesiredSize가 되도록 설정했습니다.
사실 이 강좌의 제목은 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에 관련된 메소드들이죠. 각각의 메소드들이 무슨 일을 하는지 알아보죠.
- IsItemItesOwnContainerOverride(object item)
간단히 사용자가 넣어준 아이템이 Container를 가지고 있는지 확인합니다. base.IsItemItesOwnContainerOverride(item) 에서는 item이 ListBoxItem 인지 확인하고 ItemsControl을 바로 상속했을 경우에는 base에서 itemdl UIElement 인지 확인합니다.
return 값이 false 인 경우 다음에 GetContainerForItemOverride 메소드가 호출되고 true일 경우 PrepareContainerForItemOverride 가 바로 호출 됩니다.
- GetContainerForItemOverride()
Container를 반환해줍니다. base.GetContainerForItemOverride() 에서는 ListBoxItem의 새로운 인스턴스를 반환합니다. 이 때 ItemContainerStyle 이 null 이 아닐 때는 새로 생성되는 ListBoxItem에 ItemContainerStyle의 Style이 적용됩니다.
- 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 처리를 해줍니다.
- OnItemsChanged(NotifyCollectionChangedEventArgs e)
이 메서드는 사용자가 ItemsSource 의 데이타를 INotifyCollectionChanged 인터페이스를 상속받은 데이타 클래스(ex. ObervableCollection<T>: 이 클래스의 사용방법은 차후에 설명하도록 하겠습니다. )로 했을 경우에만 들어옵니다. INotifyCollectionChanged의 CollectionChanged 이벤트가 발생하였을 경우에 들어옵니다.
- 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을 상속받습니다.
{ }
그리고 여기서 기본 Style도 조금은 바꾸어 주어야 할 것이므로 DefaultStyle도 생성자에서 설정해주어야 합니다. 다음과 같이
{
DefaultStyleKey = typeof(CheckableListBoxItem);
}
이 부분은 나중에 Control을 다 만든 다음에 자주 까먹을 수 있는 부분이니 무조건 처음부터 설정해주도록 합시다. (삽질 방지 습관화!!!)
그 다음에 TemplatePart 에 CheckBox 하나를 추가해주도록 합시다. CheckBox 객체의 이름은 왠만하면 const 값으로 박아 놓고 쓰는게 좋겠죠. 그래서 다음과 같이 짜놓았습니다.
(클래스 속성)
internal const string CheckBoxName = "CheckBox";
그리고 OnApplyTemplate에서 Style에 들어있을 CheckBox를 찾아와야 하겠죠. 그리고 찾아온 CheckBox가 Checked 되었는지 안되었는지 확인하기 위해서 이벤트를 엮어줍니다.
{
base.OnApplyTemplate();
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);
}
{
FireItemChecked(e);
}
internal void FireItemUnchecked(RoutedEventArgs e)
{
if (ItemUnchecked != null)
ItemUnchecked(this, e);
}
{
if (ItemChecked != null)
ItemChecked(this, e);
}
자 그럼 앞에서 배운 override 함수를 활용하여 CheckableListBox를 만들어 보죠. 먼저 앞서 만든 것처럼 CheckableListBox를 만들고 DefaultStyle등을 만들어 줍니다. 그리고 맨먼저 ListBoxItem 이 아닌 CheckableListBox가 ListBoxItem으로 만들어지게 하기 위해 override 함수를 수정해주어야 합니다.
먼저 OnItemItsOwnContainerOverride 함수의 경우 ListBoxItem인지 체크해주는 함수입니다. 하지만 여기서는 CheckableListBoxItem이어야 하니.. CheckableListBoxItem이 아닌 경우 Container를 새로 만들어주어야 합니다.
고로 다음과 같이 짜줍니다.
{
return (item is CheckableListBoxItem);
}
간단하죠?^^ 다음으로 item이 ChekableListBoxItem이 아닌 경우 새로 Container를 만들어 줘야 하니 GetContainerForItemOverride 함수를 수정해주어야 합니다. 다음과 같이 짭니다.
{
CheckableListBoxItem item = new CheckableListBoxItem();
if (this.ItemContainerStyle != null)
{
item.Style = this.ItemContainerStyle;
}
return item;
}
CheckableListBoxItem을 새로 만들어주고 ItemContainerStyle을 적용해주는 것이죠. 이렇게 하면 다음에 짜줄 PrepareContainerForItemOverride 함수의 DependencyObject 로 CheckableListBoxItem이 들어오는 것을 확인할 수 있습니다. 그러면 이제 PrepareContainerForItemOverride 를 수정해봅시다.
{
base.PrepareContainerForItemOverride(element, item);
if (checkableItem != null)
{
checkableItem.ItemChecked += new RoutedEventHandler(checkableItem_ItemChecked);
checkableItem.ItemUnchecked += new RoutedEventHandler(checkableItem_ItemUnchecked);
}
}
그런데 여기서 Checked 된 것이 어떤 것인지 알기 위해서는 Check된 상태를 알려줄 Event와 Check된 객체를 담아둘 List가 하나 필요할 것입니다. 그리고 Check된 객체가 원래 어떤 아이템이었는지도 알아볼 수 있는 Dictionary도 하나 그래서 다음과 같은 Event와 Property 등을 추가합니다.
public ObservableCollection<object> CheckedItems { get; private set; }
private Dictionary<CheckableListBoxItem, object> _oDicCheckableListBoxItem;
{
DefaultStyleKey = typeof(ListBox);
CheckedItems = new ObservableCollection<object>();
_oDicCheckableListBoxItem = new Dictionary<CheckableListBoxItem, object>();
}
그리고 아까 PrepareContainerForItemOverride함수에서 추가시켜주었던 EvnetHandler부분을 건드려 줍니다.
{
object item = _oDicCheckableListBoxItem[(sender as CheckableListBoxItem)];
if (CheckedItems.Contains(item))
CheckedItems.Remove(item);
}
{
CheckedItems.Add(_oDicCheckableListBoxItem[(sender as CheckableListBoxItem)]);
FireCheckItemsChanged();
}
{
if (CheckedItemsChanged != null)
CheckedItemsChanged(this, null);
}
자 그럼 이제 마지막(?) Themes/generic.xaml을 추가해주고 그 곳에 CheckBox가 추가된 CheckableListBoxItem Style을 넣어줍니다. 다음과 같이..
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">
<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을 넣어주었습니다. 그리고 코드는 다음과 같이..
{
InitializeComponent();
myListBox.ItemsSource = new ObservableCollection<object>(){ "하나","둘","셋"};
resultListBox.ItemsSource = myListBox.CheckedItems;
}
{
(myListBox.ItemsSource as IList).Add(new Color());
}
{
(myListBox.ItemsSource as IList).RemoveAt(0);
}
그런데...!!!! Check상태로 Delete를 누른 객체가 사라지지 않는다는 것을 알 수 있습니다.... 흠... 그렇습니다. 우리가 사용하지 않은 하나의 override 함수를 더 사용해야 합니다. 앞서 강조했던 ClearContainerForItemOverride 함수입니다. 이 함수에서 엮어주었던 이벤트를 해제시켜주고 참고하고 있던 리스트에서 삭제해주는 작업을 해주어야 합니다. 다음과 같이요.
{
base.ClearContainerForItemOverride(element, item);
if (checkableItem != null)
{
object item2 = _oDicCheckableListBoxItem[checkableItem];
if (CheckedItems.Contains(item2))
{
CheckedItems.Remove(item2);
}
checkableItem.ItemUnchecked -= new RoutedEventHandler(checkableItem_ItemUnchecked);
_oDicCheckableListBoxItem.Remove(checkableItem);
}
}
급하게 ListBox를 상속받아서 Customizing 하다보면 항상 이 부분을 놓치기 쉽습니다. 이 부분이 구현이 안되면 생각보다 오작동하는 경우가 많기 때문에 꼭 짜주는 것이 좋습니다.
흠.. 그림 한장 없이 설명하다보니 이해가 쉽게 되지 않을 수도 있다는 생각이 듭니다. 그림을 넣기 좀 애매한 부분이 있어서.^^;;; 지금 여기서 설명한 method들은 모두 ItemsControl 에서 상속받은 method들이기 때문에 ListBox가 아니라 바로 ItemsControl 을 상속받아 Class를 만들 때에도 적용이 가능한 것들입니다. 이해를 돕기 위해 샘플프로젝트를 첨부합니다. 그럼 모두 삽질 금지!!^^
- smile -
바로 ItemContainerStyle 을 바꾸어 주는 방법과 ItemTemplate을 바꾸는 방법이지요.
ItemTemplate 을 바꾸는 방법은 스캇 구슬리 강좌 에 소개 되어있는데요. 기본적으로 Container 안에 내용물을 바꾸는 것으로 볼 수 있죠.
그럼 ItemContainerStyle부터 살펴 봅시다. ItemContainderStyle은 Style을 Value 값으로 받습니다. 아시는 분은 알겠지만 Style은 TargetType 이 필요하죠. 여기서 TargetType은 ListBoxItem 입니다. 결국 ItemContainerStyle은 ListBoxItem의 스타일을 정해주는 프로퍼티라는 것이지요.
그럼 ListBoxItem의 DefaultStyle을 볼까요?
<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부분만 간단히 정리해보죠.
<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.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Ellipse Width="5" Height="5" Margin="5" Fill="Black"/>
<TextBlock Text="{Binding }"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
이 후 비하인드 코드에서 다음과 같이 셋팅해줍시다.
결과는 다음과 같죠.
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이 생겨 있음을 볼 수 있습니다.
그럼 다음에는 ItemsPanel 에 대해 설명하죠. 다음 장에는 좀더 자세히 설명을 해야겠다는 반성을.....--;;
- smile -
스캇 구슬리의 영문 강좌
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(myEllipse);
이렇게 아이템을 셋팅했을 때의 좋은 점은 소수의 아이템을 셋팅 시 간편하고 추가시킨 UIElement를 MyList.Items Collection을 통해 직접 가지고 올 수 있다는 것입니다.
그런데 이렇게 Item을 셋팅시켰을 때는 한가지 문제점이 생깁니다. 한번 Item으로 셋팅된 Item은 다시는 다른 곳에 셋팅시킬 수 없다는 것입니다. 이것은 버그성 같기도 하지만 어쨋든 다음과 같은 코드는 에러를 수반합니다.
이에 대한 것은 다른 글에 포스팅을 이미 했으니 참고 하시길 바랍니다.
http://error1001.com/16
두번째 방법은 위의 방법보다 정상적인 방법이라고 하겠습니다. 바로 ItemsSource 에 DataClass 의 Collection 을 셋팅하는 방법입니다.
간단히 정리하면...
MyListBox.ItemsSource = collection;
뭐 이렇게 된다는 것이죠.
여기서 약간은 복잡한 루틴이 들어가는데.. 먼저 다음과 같은 코드를 써보죠.
결과는 다음과 같습니다.
내부적으로 이루어지는 코드를 살펴 보면 다음과 같습니다. (아래는 beta1때 공개된 Mix Control 소스입니다. 현재버전과는 조금 상이한 부분이 있습니다. 하지만 내부적인 구현은 비슷합니다.)
{
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 -
바로 리스트 박스로 들어가자고 하니 초반부터 낙오자가 많을 것같다는 생각이 들더군요. 그래서 아주 기본 컨트롤부터 시작하기로 했습니다.
ContentControl은 아주 유용한 Control이면서 아주 기본적인 Control입니다. 우리가 알고 있는 대부분의 Control이 ContentControl을 상속받고 있죠. 일단 모든 Button류가 상속하고 있고 ListBoxItem등도 상속하고 있는 클래스입니다.
그럼 이것이 도데체 어떤 컨트롤인지 알아보기 위해 한번 화면에 뿌려보도록 하겠습니다.
다음처럼 간단한 코드를 Xaml에 추가해보죠..
화면에 단순히 "This is a ContentControl!!" 이라고 뿌려지는 것을 볼 수 있을 겁니다.
뭐야... 그냥 TextBlock 인거야?... 하지만 다음과 같은 코드를 뿌려보면 단번에 뭐하는 녀석인지 알 수 있습니다.
<ContentControl.Content>
<Ellipse Width="50" Height="50" Fill="Purple"/>
</ContentControl.Content>
</ContentControl>
결과는 단순히 보라색 동그라미가 화면에 뿌려지는 것일 겁니다. (여기서 ContentControl.Content 태그는 생략해도 상관없습니다.)
아하 그러면 결국 이놈은 그냥 Content를 표시해주는 역활을 해주는 것이군요...
사실 좀더 세부적으로 어떻게 구현되는 가를 살펴보기 위해 ContentControl의 Template을 살펴보도록 하죠.
<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을 가지고 있는 것을 알 수 있습니다.
"<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 라는 메소드에서는 다음과 같은 작업을 수행합니다.
- 먼저 Default Template을 통해 들어있던 객체를 Grid로부터 제거 합니다.
- 그리고 새로 받은 Template이 있으면 Template을 Content에 UIElement가 있으면 Content를 엘리먼트에 집어넣어줍니다.
- 새로 셋팅된 Template도 없고 Content가 UIElement도 아닌 경우 Default Template에 있던 TextBlock의 Text값에 Content를 ToString 해서 넣어줍니다.
<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>
<ContentControl.ContentTemplate>
<DataTemplate>
<Grid>
<Ellipse Width="200" Height="50" Fill="LightSteelBlue"/>
<TextBlock Text="{Binding Tag}" />
</Grid>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
{
public Page()
{
InitializeComponent();
contentControl.Content = new TestData() { Tag = "This is a ContentControl!!!" };
}
}
public class TestData
{
public string Tag { get; set; }
}
1. ListBox 의 기본 적인 사용법
2. ListBox 의 확장.(ListBox 상속 받아 쓰기)
3. Listbox Clone 만들기.
4. RichListBox 만들기.
이 중 1, 2, 3 번은 확정이고 4번은 고민중입니다. 아직 구현된 component가 제대로 작동하는지 충분히 테스트 해보지 못했기 때문입니다. 그럼 이제 시작해보죠.
- smile -
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);
}
{
선택하자 마자. 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);
}
{
list.SelectedItem = null;
}
{
list2.SelectedItem = null;
}
결과는... 일단 처음에 list에 3을 클릭하고 list2의 4를 클릭했을 때 분명히 list의 3이 선택해제가 되었습니다. 오.. 이것은 되는 구나 했지만... 그다음에 바로 오작동이 시작되었습니다. 다시 list의 3을 클릭했을 때 list2의 4는 선택해제가 되지 않았습니다. 뿐만 아니라 그 이후에 list의 3을 클릭해도 list2의 4를 클릭해도 전혀 SelectionChaged 이벤트가 들어오지 않더군요..
이 오작동을 가지고 Gilbert가 실제로 구현해 놓은 부분에 가서 확인을 해보았습니다. 그런데 정말 신기하게도 이런 오작동도 발견되지 않더군요. 그래서 다음처럼 구현을 해보았습니다.
{
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);
}
{
if (e.AddedItems.Count == 0)
return;
{
list2.SelectedItem = null;
}
else
{
list.SelectedItem = null;
}
}
list와 list2에서 같은 EventHandler를 사용하는 것이죠. 이렇게 하는 것이 무슨 차이점이 있는지 알 수 없지만 Gilbert군이 구현해놓은 코드에는 이런식으로 구현이 되어있었습니다.
결과는.... 정말 잘 작동합니다....
이게 잘 작동하는 이유는 정말 알 수가 없군요...
결국 정리하자면 이렇습니다.
SelectionChaged 이벤트에서 listBox의 Selection을 바꿔주고 싶을 때는 왠만하면 Dispatcher를 쓰자!!!!
그럼 모두 삽질 덜하시길...^^
- smile -
FontSource 를 셋팅하는 방법은 다음과 같습니다.
tbText.FontSource = new FontSource(stream);
여기서 stream은 보통 폰트 화일을 압축한 zip화일을 WebClient로 불러와서 설정해주게 되죠.
여기서 주의 할 점.
Font 화일이 화일명이 한글 화일이면 폰트가 제대로 적용안된다는 것입니다.
쉽게 말해.
'나눔고딕.ttf' 이런 화일을 압축해서 "NanumGothic.zip" 화일로 압축했다고 하면.
이 zip 화일의 stream 을 FontSource에다가 넣어준 경우 Font stream을 제대로 얻어오지 못한다는 것입니다.
'NanumGothic.ttf'로 화일명을 변경후 압축해서 WebClient를 통해 Stream을 받으면 정상 작동하게 되죠..^^
그럼 모두 삽질 금지!!!!











간단하게 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 -비쥬얼 스튜디오를 쓰다보면 가끔 이런 에러가 뜰 때가 있습니다.
그런데 실행은 잘 되죠.
테스트 결과.
App.Xaml에서 스타일이 정의가 되어 있고, 그 스타일을 사용한 객체를 A라고 하고,
A를 포함한 어떤 유저컨트롤을 B라고 가정합니다.
이때 유저컨트롤 B를 가지고 있는 객체 C를
비주얼 스튜디오에서 미리보기 하는 순간 위와 같은 에러를 반환합니다.
Preview상의 버그이고 프로그램에는 전혀 지장을 안 주는 것 같으니 무시합시다. --;;
- smile -
예제를 첨부하죠.
dll 은 여기서 다운 받습니다.
이건 클래스 템플릿, \Documents\Visual Studio 2008\Templates\ItemTemplates\Visual C#\Silverlight
요 위치에 붙여 넣습니다.
이건 프로젝트 템플릿, \Documents\Visual Studio 2008\Templates\ProjectTemplates\Visual C#\Silverlight
요 위치에 붙여 넣습니다. 프로젝트가 위에 dll 위치를 못찾을 수 있으므로 dll을 제대로 추가해준 뒤
다시 export하여 zip 화일을 같은 경로에 붙여 넣어줍시다.
자 이제 유닛테스트로 개발 해봅시다.
- smile -
사진출처 : flickr.com
포팅작업 돌입!
얼마전 실버라이트 RC0가 공개되어,
휴즈플로우의 은대리는 이전에 만들어 둔 프로젝트를 포팅하는 작업에 들어갔다.
컴파일과 디버깅을 거듭한 끝에 드디어 컴파일 에러 제로!
근데 실행을 시켜 본 순간, 이게 무슨 문제인가?
App.xaml.cs의 InitializeComponent()에서 런타임 에러가 발생한다.
App.xaml을 열자 잘못된 부분에 밑줄이 그어지면서 VS가 이곳저곳 오류를 보고해준다.
'아... ContentTemplate가 Control 부모를 버리고 FrameworkElement에게 입양 되었었지...'
그 결과 많은 프로퍼티들이 사라졌으므로 오류가 발생하는 것이다.
은대리는 FontStyle 등 밑줄이 그어진 많은 프로퍼티를 XAML 코드에서 삭제해 나갔다.
그리고 VisualTransition의 Duration도 잊지않고 GeneratedDuration으로 바꿔주었다.
오류가 눈앞에서 모두 사라졌다.
마지막 고비
이번엔 프로그램이 뜰까? 은대리는 다시 실행해본다...
다시 또 오류다.
마지막 문제는 Visual Studio가 힌트를 주지 않는다.
은대리의 삽질을 막고자하면 아래의 팁을 알려주라!
1. vsm:Style 엔티티를 Style로 Replace 한다.
2. vsm:Setter 엔티티를 Setter로 Replace 한다.
자, 이제 프로그램이 잘 뜬다.
일단 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도 동적 제어가 가능합니다.
착한 외국분이 이미 개발해 주셨군요. 감사히 가져다 씁시다.
사용법도 아주 간단합니다.
이건 혹시나 링크가 없어질까봐.. 첨부합니다.
- smile -
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"파일을 사용한 커스텀 커서를 허용해줬으면 좋겠어요.
오늘도 또 실버라이트의 치부를 공개하게 되는군요.
어제 저는 원하지도 않는 기능을 실버라이트가 지원하고 있다는 사실을 알게 되었습니다.
바로 SpaceBar Click 입니다. 이것은 ButtonBase를 상속한 모든 객체가 적용됩니다.
실제로 Button에 Focus가 가있는 상태에서만 작동을 해서 문제가 되지 않을 수 있지만 다음에
경우 크게 문제가 됩니다.
Button 클릭으로 어떤 새로운 객체를 띄워야 하는 경우.. 이 경우 만약 버튼이 새로운 객체에 의해
가려진다면 실제 의도상으로는 새로운 객체는 한개만 띄워져야 하지만 SpaceBar를 사용하면 여러번
띄워지는 경우가 생기죠.
이 경우는 개발자가 부주의 한 탓에 생긴 버그라고 할 수 있죠.. 이런 경우 클릭 이후 Focus를 새로 띄운
객체에게 다시 맞춰줘야겠죠.
다음의 경우는 버그가 좀더 심각합니다. 간단한 프로젝트를 첨부하니 한번 확인해 보시길 바랍니다.
테스트 방법은 간단합니다.
Button을 하나 만들고 Click 시 이 버튼의 Visibility 를 Collapsed 시켜줍니다.
그리고 Spacebar를 눌러서 Click 이벤트를 발생시킵니다.
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를 받으니 이런 문제가 생기는군요.
암튼 간단히 결론만 말씀드리자면 다음과 같습니다.
오늘은 결론이 간단하네요.^^ 그럼 Focus관리 잘하셔서
모두들 삽질 덜하시길..^^
- smile -
이번에 보고할 버그는 굉장히 심각한 버그입니다. 실버라이트에서 일반적인 한글 경로의 처리문제인데요.
"당연히 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이 되지 않도록 하자는 것이었어요..
결국 방법은 다음과 같죠.
클라이언트에서 값을 보낼때는 다음과 같이 보냅니다.
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 -
위의 프로젝트는 여러객체의 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 -
원문 : 길버라이트 (http://gilverlight.net/2917)
조금 황당한 경우입니다.
이런 문제를 마주치시더라도 당황하지 마세요.
아시는 바와 같이 Visual Studio에서 실버라이트 프로젝트를 생성하면
*.aspx 와 *.html 견본페이지가 생성됩니다.
특히 *.html 페이지에 보면 object를 사용하여 실버라이트를 호스팅하는
부분이 있습니다.
보통 아래와 같습니다.
<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 함부로 떼면,
실버라이트 런타임을 또 깔으라고 하네요.
이.상.하.죠? ^^
결론
(당분간은 말이죠.) 마지막의 ,(comma)를 함부로 제거하지 맙시다.
Error 1001.
자세히 보기
보통 버튼을 만들 때 이미지 두장 혹은 이미지 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
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
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을 만들려고 합니다.
일단 기본적으로는 버튼하고 똑같고 위의 Style을 그대로 따르지만 Image의 Source만 다른 프로퍼티로 바꿀 수 있었으면 좋겠죠.
예전의 짱묜님의 강좌에서는 이미지가 한장뿐이어서 Tag를 사용해서 해결했지만 이경우는
3개나 되니.. 그 방법은 쓰기가 힘들죠.
그럼 일단 ImageButton을 만들어 보겠습니다. 간단히 class 를 하나 만드십쇼.
ImageButton이란 이름으로 말이죠. 그리고 Button을 상속 받습니다.
{}
테스트 해보시면 알겠지만 이상태로도 바로 Button처럼 작동하게 됩니다.
Page.xaml
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로 해줍니다.
그럼 다음과 같은 코드가 추가 될 것입니다.
/// Gets or sets the NormalImageSource possible Value of the Uri object.
/// </summary>
public ImageSource NormalImageSource
{
get { return (ImageSource)GetValue(NormalImageSourceProperty); }
set { SetValue(NormalImageSourceProperty, value); }
}
/// Identifies the NormalImageSource dependency property.
/// </summary>
public static readonly DependencyProperty NormalImageSourceProperty =
DependencyProperty.Register(
"NormalImageSource",
typeof(ImageSource),
typeof(ImageButton),
new PropertyMetadata(OnNormalImageSourcePropertyChanged));
/// 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
/// Gets or sets the OverImageSource possible Value of the Uri object.
/// </summary>
public ImageSource OverImageSource
{
get { return (ImageSource)GetValue(OverImageSourceProperty); }
set { SetValue(OverImageSourceProperty, value); }
}
/// Identifies the OverImageSource dependency property.
/// </summary>
public static readonly DependencyProperty OverImageSourceProperty =
DependencyProperty.Register(
"OverImageSource",
typeof(ImageSource),
typeof(ImageButton),
new PropertyMetadata(OnOverImageSourcePropertyChanged));
/// 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
/// Gets or sets the PressedImageSource possible Value of the Uri object.
/// </summary>
public ImageSource PressedImageSource
{
get { return (ImageSource)GetValue(PressedImageSourceProperty); }
set { SetValue(PressedImageSourceProperty, value); }
}
/// Identifies the PressedImageSource dependency property.
/// </summary>
public static readonly DependencyProperty PressedImageSourceProperty =
DependencyProperty.Register(
"PressedImageSource",
typeof(ImageSource),
typeof(ImageButton),
new PropertyMetadata(OnPressedImageSourcePropertyChanged));
/// 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로 해주어야 합니다.
자 그러면 이제 내용을 살펴 볼까요.
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" 로 둡니다.
여기까지 잘 따라오셨다면 다음과 같은 코드가 생깁니다.
<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 로 돌아와서 생성자를 만들어줍니다.
{
DefaultStyleKey = typeof(ImageButton);
}
여기까지만 해도 일단 집어넣은 이미지가 있으니 작동을 하지요.
그런데 여기서 우리가 원하는 것은 프로퍼티 값을 바꾸어서 얼마든지 Image를 바꿀 수 있게
하는 것이죠. 자 그럼 Template 부분을 이렇게 바꾸어 봅시다.
<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="OverImage" Source="{TemplateBinding OverImageSource}" Stretch="Fill" Visibility="Collapsed"/>
<Image x:Name="PressedImage" Source="{TemplateBinding PressedImageSource}" Stretch="Fill" Visibility="Collapsed"/>
쓸데없는 프로퍼티들은 일부러 제거 했습니다.
그리고 이제 우리가 애초에 원하던 대로 코드를 바꾸어 봅시다.
Page.xaml
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 을 새로
생성해서 반환해주는 코드를 짜주어야 합니다.
{
get { return new BitmapImage(new Uri((GetValue(NormalImageSourceProperty) as BitmapImage).UriSource.OriginalString, UriKind.RelativeOrAbsolute)); }
set { SetValue(NormalImageSourceProperty, value); }
}
뭐 이정도 소스가 되겠죠..^^;;
이렇게 글로 설명하는데는 별로 익숙하지 못해서 잘 이해가 되셨는지 모르겠군요.
자 그럼 이제 Custom Control의 세계로...^^~~~
- smile -

UIAutomationTest(2).zip
ContentPresenter.cs
Microsoft.Silverlight.Testing.dll
Prev
Rss Feed