성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>WPF - 다중 스레드 환경에서 데이터 바인딩의 INotifyPropertyChanged.PropertyChanged에 대한 배려</h1> <p> 제목 짓기가 난감하군요. ^^<br /> <br /> 일단, 이 글은 다음의 질문 때문에 쓰게 되었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 스레드 관련해서... ; <a target='tab' href='https://social.msdn.microsoft.com/Forums/ko-KR/eb2a30fb-d300-4376-a1d2-d7e4b1465521/-?forum=dotnetko'>https://social.msdn.microsoft.com/Forums/ko-KR/eb2a30fb-d300-4376-a1d2-d7e4b1465521/-?forum=dotnetko</a> </pre> <br /> 질문자의 의도와는 달리 저는 오히려 INotifyPropertyChanged.PropertyChanged로 발생한 이벤트가 스레드 경계를 넘는 데이터 바인딩으로 이뤄졌을 때 오류가 발생하지 않는 것이 이상했습니다. ^^<br /> <br /> 이해를 돕기 위해 재현 코드를 작성해보겠습니다.<br /> <br /> 다음과 같이 INotifyPropertyChanged를 구현한 클래스를 정의하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class UserModel : <span style='color: blue; font-weight: bold'>INotifyPropertyChanged</span> { private int _Age; public int Age { get { return _Age; } set { _Age = value; RaisePropertyChanged("Age"); } } <span style='color: blue; font-weight: bold'>public event PropertyChangedEventHandler PropertyChanged;</span> protected virtual void RaisePropertyChanged(String propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } protected void OnPropertyChanged(PropertyChangedEventArgs e) { var handler = this.PropertyChanged; if (handler != null) { <span style='color: blue; font-weight: bold'>handler(this, e);</span> } } } </pre> <br /> MainWindow.xaml.cs에 UserModel 인스턴스를 Window의 DataContext에 설정 및 별도의 스레드에서 값을 변경해 줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public partial class MainWindow : Window { private UserModel _user; public MainWindow() { <span style='color: blue; font-weight: bold'>_user = new UserModel(); this.DataContext = _user;</span> this.Loaded += MainWindow_Loaded; InitializeComponent(); } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { <span style='color: blue; font-weight: bold'>Thread t1 = new Thread(worker_DoWork);</span> t1.Start(); } private void worker_DoWork() { while (true) { <span style='color: blue; font-weight: bold'>_user.Age += 1;</span> Thread.Sleep(1000); } } } </pre> <br /> 아울러 MainWindow.xaml에서 Age 속성에 대해 TextoBlock으로 다음과 같이 바인딩을 해줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApplication1" xmlns:z="clr-namespace:WpfApplication1" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel Orientation="Horizontal"> <TextBlock Text="User Age : " /> <span style='color: blue; font-weight: bold'><TextBlock Text="{Binding Path=Age}" /></span> </StackPanel> </Grid> </Window> </pre> <br /> 이제 실행하면, worker_DoWork 메서드가 별도의 스레드에서 실행되므로 Age 속성을 변경하는 경우, INotifyPropertyChanged.PropertyChanged 이벤트가 발생합니다. 정상(?)적이라면, 이벤트는 delegate 호출이므로 스레드 경계를 넘게 되고 이를 바인딩하고 있는 TextBlock은 다른 스레드에서 생성한 것이므로 당연히 예외가 발생해야 합니다. 그런데, 발생하지 않습니다. ^^<br /> <br /> 살짝 예제를 바꿔서, _user 인스턴스의 PropertyChanged 이벤트를 Main Window에서 구독해보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public MainWindow() { _user = new UserModel(); this.DataContext = _user; this.Loaded += MainWindow_Loaded; <span style='color: blue; font-weight: bold'>_user.PropertyChanged += User_PropertyChanged;</span> InitializeComponent(); } private void User_PropertyChanged(object sender, PropertyChangedEventArgs e) { <span style='color: blue; font-weight: bold'>this.Title = (sender as UserModel).Age.ToString();</span> } </pre> <br /> "this.Title" 접근 코드에서 예상했던 그 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > An unhandled exception of type 'System.InvalidOperationException' occurred in WindowsBase.dll Additional information: The calling thread cannot access this object because a different thread owns it. </pre> <br /> 그런데, 어떻게 DataBinding된 경우에는 그렇지 않은 걸까요? 아마도 스레드 경계를 넘는 경우 자동으로 WPF 측에서 대상 UI를 생성한 스레드로 전달해주는 배려가 되어 있지 않을까 예상할 수 있습니다. 음... 예상만 하지 말고 ^^ 직접 확인을 해볼까요?<br /> <br /> 이를 위해 닷넷 소스 코드 디버깅을 위한 다음의 환경 구성이 필요합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 소스 코드가 없는 닷넷 어셈블리를 디버깅할 때 지역 변수 값을 확인하는 방법 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11036'>http://www.sysnet.pe.kr/2/0/11036</a> </pre> <br /> 위의 설정으로 Visual Studio 2015를 실행, PropertyChanged 인스턴스인 handler를 호출하는 곳에서부터 F11(Step-into) 디버깅을 하면 됩니다. DataBinding된 경우 PropertyChanged를 직접 구독하고 있는 객체는 System.ComponentModel.PropertyChangedEventManager이고, 그것의 OnPropertyChanged 핸들러가 1차적으로 이벤트에 반응합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class PropertyChangedEventManager : WeakEventManager { private static readonly string AllListenersKey = "<All Listeners>"; private PropertyChangedEventManager() { } // ...[생략]... <span style='color: blue; font-weight: bold'>private void OnPropertyChanged(object sender, PropertyChangedEventArgs args)</span> { // ...[생략]... try { <span style='color: blue; font-weight: bold'>base.DeliverEventToList(sender, args, empty);</span> } finally { empty.EndUse(); } } // ...[생략]... } </pre> <br /> 그리고 이어지는 호출은 System.Windows.WeakEventManager.DeliverEventToList를 거쳐 IWeakEventListener 인터페이스를 구현한 MS.Internal.Data.PropertyPathWorker 객체의 ReceiveWeakEvent로 연결됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public abstract class WeakEventManager : DispatcherObject { protected void DeliverEventToList(object sender, EventArgs args, ListenerList list) { bool flag = false; Type managerType = base.GetType(); int num = 0; int count = list.Count; while (num < count) { IWeakEventListener listener = list[num]; if (listener != null) { bool condition = <span style='color: blue; font-weight: bold'>listener.ReceiveWeakEvent(managerType, sender, args);</span> // ...[생략]... } // ...[생략]... } if (flag) { this.ScheduleCleanup(); } } // ...[생략]... } </pre> <br /> ReceiveWeakEvent에서는 다시 MS.Internal.Data.ClrBindingWorker 객체의 OnSourcePropertyChanged 메서드를 호출하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal sealed class PropertyPathWorker : IWeakEventListener { // ...[생략]... bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { // ...[생략]... if (managerType == typeof(PropertyChangedEventManager)) { PropertyChangedEventArgs args = (PropertyChangedEventArgs) e; <span style='color: blue; font-weight: bold'>this._host.OnSourcePropertyChanged(sender, args.PropertyName);</span> } // ...[생략]... return true; } // ...[생략]... } </pre> <br /> 마지막으로, 다음과 같이 현재 스레드에서 이벤트가 발생하고 있는지의 여부를 테스트하는 코드가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class ClrBindingWorker : BindingWorker { // ...[생략]... internal void OnSourcePropertyChanged(object o, string propName) { int num; if (!base.IgnoreSourcePropertyChange && ((num = this.PW.LevelForPropertyChange(o, propName)) >= 0)) { <span style='color: blue; font-weight: bold'>if (base.Dispatcher.Thread == Thread.CurrentThread)</span> { this.PW.OnPropertyChangedAtLevel(num); } else { base.SetTransferIsPending(true); <span style='color: blue; font-weight: bold'>base.Dispatcher.BeginInvoke</span>(DispatcherPriority.DataBind, new DispatcherOperationCallback(this.ScheduleTransferOperation), new object[] { o, propName }); } } } // ...[생략]... } </pre> <br /> 보는 바와 같이, 같은 스레드라면 일반적인 delegate 호출로 연결되는 반면 다른 스레드라면 우리가 익히 알고 있는 Dispatcher.BeginInvoke를 통해 대상 스레드에서 호출되도록 하고 있습니다.<br /> <br /> 그렇습니다. 마법은 없습니다. ^^ WPF의 이런 코드 덕분에 개발자는 별다른 부가 코드 없이 스레드 경계에 상관없이 INotifyPropertyChanged.PropertyChanged 이벤트를 발생시킬 수 있었던 것입니다.<br /> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1064&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1719
(왼쪽의 숫자를 입력해야 합니다.)