'Tip'에 해당되는 글 15건
- 2009.05.06 Visual Studio에서 XAML을 열 때 [미리보기] 안하기 1
- 2009.01.09 실버라이트에서의 UI 자동화 - Part 2 (쉬운 방법)
- 2009.01.09 [번역] 실버라이트에서의 UI Automation - 사용자 인터랙션 시뮬레이션
- 2009.01.05 UI Automation에 관련된 글. 1
- 2009.01.02 Dependecy Property에 관한 블로그
- 2009.01.02 Ninjection 에 대한 포스트 및 동영상
- 2008.12.02 ListBox의 Select된 객체 해제하기.(Select취소하기) 1
- 2008.11.05 Wheel 지원 리스트 박스
- 2008.10.06 Silverlight Unit Test Template 1
- 2008.09.29 팁 : 실버라이트 2 RC0 포팅 시 Style에서 발생하는 오류
- 2008.09.19 VisualState 의 동적 제어.
- 2008.09.11 실버라이트에서 MD5 암호화
- 2008.07.03 VisualTreeHelper 가 생겼군요.^^
- 2008.06.13 [SL2Beta2] generic.xaml 의 namespace
- 2008.06.12 바뀐 Uri.!! 나만 모른겨!!?
원문 : 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 보기가 부담스럽지 않다!감사합니다.
<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 -
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 -
간단하게 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 -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 -
유용하게 사용합시다.
VisualTreeHelper.GetChild 의 경우는 두개의 파라미터를 받는데
하나는 DependencyObject 이고 하는 ChildIndex 입니다.
보통 자식 객체는 Panel의 Children을 통해서나 ContentControl의 Content를
통해서만 가지고 올 수 있었는데 VisualTreeHelper를 통하면 위의 제약에 상관없이
자식 객체를 가지고 올 수 있습니다. 뿐 만 아니라.
GetParent 로는 Parent도 마음대로 가지고 올 수 있으니 실제로 화면에 보이는
모든 객체를 가지고 올 수 있을 것 같군요.
그럼 유용하게 사용하시길 바랍니다.
- smile -
이걸로 합시다.
이걸로 해야. SilverlightDefaultStyleBrowser 에서 Style을 확인할 수 있습니다.
그리고 이렇게 해야 이걸 카피해서 Blend에서 Style을 편집할 수 있을 테니까요.
아직까지 Custom Control들의 Style은 Blend에서 Edia a copy로 복사 되지 않습니다.
결국 사용자가 직접 카피해 붙여야 한다는 이야기죠..
좀 불편해도.. beta2니...^^;;
- smile -
Uri 사용방법이 바뀐 것 같습니다. 제대로 바뀐 것 같네요..
예전에는 root에 접근 할 수 가 없었는데 이제는
new Uri("/화일 이름", UriKind.Relative) 로 설정해두면.
root에서 화일을 찾습니다. ClientBin 폴더가 아니라요.
하지만
new Uri("화일 이름", UriKind.Relative) 로 설정해두면.
ClientBiin 폴더. xap 화일이 있는 경로부터 시작합니다.
유념에 두시고 쓰세요.
이팀장 말대로는 이제야 제대로 된 거라고 하는군요.. ^^;;;
- smile -