C# 코딩연습 대리자와이벤트 2007-12-13 김과장 (kimgwajang@hotmail.com)
A. 대리자 사람을나타내는간단한 Person 클래스로부터시작하겠습니다. 01 public class Person 03 private int _age; 04 05 public int Age 06 { 07 get { return _age; } 08 set { _age = value; } 09 } 10 11 private string _name; 12 13 public string Name 14 { 15 get { return _name; } 16 set { _name = value; } 17 } 18 19 public Person(int age, string name) 20 { 21 _age = age; 22 _name = name; 23 } 24 } 코드 1 이클래스에나이를한살더하는메서드가있다고하지요. 1 public void IncreaseAge() 2 { 3 _age++; 4 } 코드 2 이제는 IncreaseAge 메서드에나이가변경되었음을알리는코드를추가하는것에 대해서생각해봅시다. 1
1 public void IncreaseAge() 2 { 3 int oldage = _age; 4 _age++; 5 6 Console.WriteLine(string.Format("{0} -> {1}", oldage, _age)); 7 } 코드 3 직관적인형태라서좋긴하지만, 6 번라인이문제가됩니다. 콘솔에출력을하는 UI 로직이클래스내부에추가됨으로써이제이 Person 클래스는 Console 이라는 UI 클래스와강한결합을가지게되었습니다. 즉 Person 클래스를윈폼이나웹페이지에서사용하기가어렵다는이야기이지요. 이러한클래스간의강한결합을피하기위해서는, Person 클래스내부에서 UI 관련 코드를제거하여야합니다. 대신 Person 클래스는 UI 코드에대한참조를가지고있는 것으로충분합니다. 이러한참조를닷넷은대리자라는이름으로제공을하고있습니다. 위예를대리자를사용하는형태로변경을해봅시다. 먼저우리가하고자하는작업에 적당한대리자를선언하여야겠지요. 6 번라인을보고짐작을할때아마도다음과같은 시그니처를가지는대리자이면적당할것같습니다. public delegate void AgeChangedDelegate(int oldage, int newage); 또한 IncreaseAge 메서드는 UI 관련코드를제거하고대신대리자를호출하는형태로 바꿀수있습니다. 01 public AgeChangedDelegate AgeChanged; 02 03 public void IncreaseAge() 04 { 05 int oldage = _age; 06 _age++; 2
07 08 if (AgeChanged!= null) 09 AgeChanged(oldAge, _age); 10 } 코드 4 1 번라인과같이대리자필드를선언하고 9 번라인과같이이를호출하고있습니다. 이제이 Person 클래스를사용하는코드를생각해봅시다. 01 static void Main(string[] args) 03 Person person = new Person(32, " 김과장 "); 04 person.agechanged = new AgeChangedDelegate(Person_AgeChanged); 05 person.increaseage(); 06 } 07 08 private static void Person_AgeChanged(int oldage, int newage) 09 { 10 Console.WriteLine(string.Format("{0} -> {1}", oldage, newage)); 11 } 코드 5 C# 2.0 부터는코드 5 의 4 번라인을다음과같이줄여서쓸수있습니다. person.agechanged = Person_AgeChanged; 코드 5 의실행결과는아래와같습니다. 32 - > 33 계속하려면아무키나누르십시오... B. 무명메서드 C# 2.0 에도입된무명메서드를사용하면코드 5 는아래와같이고칠수있습니다. 01 01 static void Main(string[] args) 03 Person person = new Person(32, " 김과장 "); 3
04 person.agechanged = delegate(int oldage, int newage) 05 { 06 Console.WriteLine(string.Format("{0} -> {1}", oldage, newage)); 07 }; 08 09 person.increaseage(); 10 } 코드 6 무명메서드를사용함으로써코드가한결간결해지기도했지만, 무명메서드의진가는이이상입니다. 예를들어, 코드 6 을이전나이와새나이외에도생년까지같이표시하도록고친다고해봅시다. 이때 Person 와 AgeChangedDelegate 대리자는수정할수없는상황이라고가정을합니다. 01 static void Main(string[] args) 03 int birthyear = 1976; 04 05 Person person = new Person(32, " 김과장 "); 06 person.agechanged = delegate(int oldage, int newage) 07 { 08 Console.WriteLine(string.Format("{0} -> {1} : {2} 년생 ", oldage, newage, birthyear)); 09 }; 10 11 person.increaseage(); 12 } 코드 7 실행결과는다음과같습니다. 32 - > 33 : 1976 년생계속하려면아무키나누르십시오... 3 번라인에서선언한변수 birthyear 를무명메서드내에서사용할수있는것에 주목하여주십시오. 만일아래코드 8 와같이명명된메서드를사용한다면 Person_AgeChanged 메서드에서는 birthyear 변수에바로접근할수없습니다. yearbirth 4
변수를 Person_AgeChanged 메서드에전달하기위해서는 yearbirth 변수를클래스의 필드로만든다든지하는방법을사용하여야할것입니다. 01 static void Main(string[] args) 03 int birthyear = 1976; 04 05 Person person = new Person(32, " 김과장 "); 06 person.agechanged = Person_AgeChanged; 07 08 person.increaseage(); 09 } 10 11 private static void Person_AgeChanged(int oldage, int newage) 12 { 13 // birthyear 변수가알려지지않았기때문에컴파일에러 14 Console.WriteLine(string.Format("{0} -> {1} : {2} 년생 ", oldage, newage, birthyear)); 15 } 코드 8 C. 이벤트 위예제의경우대리자를사용하여구현하여도로직과 UI 의분리라는일차적인 목표는달성할수있지만, C# 에는이러한작업을위한지원이언어차원에서제공됩니다. 바로이벤트메커니즘이라고하는것이지요. Person 클래스를이벤트를사용하는형태로변경해봅시다. 01 public class Person 03... 04 05 public event AgeChangedEventHandler AgeChanged; 06 07 public void IncreaseAge() 08 { 09 int oldage = _age; 5
10 _age++; 11 12 if (AgeChanged!= null) 13 AgeChanged(oldAge, _age); 14 } 15 } 16 17 public delegate void AgeChangedEventHandler(int oldage, int newage); 코드 9 먼저대리자의이름을 AgeChangedDelegate 에서 AgeChangedEventHandler 로 변경하였습니다. 이는닷넷프레임웍의명명지침법을따른것입니다. 5 번라인을보면이벤트를선언하고있습니다. 12~13 라인은이전과달라진것이 없지만, 이제는대리자가아니라이벤트를호출하고있다는것도확인하시기바랍니다. 닷넷프레임웍의이벤트작성지침에의하면이벤트대리자의시그니처는아래와같은 형태를따르도록권장됩니다. public delegate void AgeChangedEventHandler(object sender, EventArgs e); 첫번째매개변수인 sender 로는해당이벤트를발생시킨객체를전달하고, 그외이벤트핸들러에서사용할값들은 EventArgs ( 또는그의자식클래스 ) 객체의필드로포함시켜두번째매개변수로전달합니다. 우리의예제에서는이전나이와새나이를멤버로가지는 AgeChangedEventAgrs 형의객체를전달하겠네요. 그렇다면 AgeChangedEventAgrs 클래스를만들어봅시다. 01 public delegate void AgeChangedEventHandler(object sender, AgeChangedEventArgs e); 02 03 public class AgeChangedEventArgs : EventArgs 04 { 05 private int _oldage; 06 private int _newage; 6
07 08 public int OldAge 09 { 10 get { return _oldage; } 11 set { _oldage = value; } 12 } 13 14 public int NewAge 15 { 16 get { return _newage; } 17 set { _newage = value; } 18 } 19 20 public AgeChangedEventArgs(int oldage, int newage) 21 { 22 _oldage = oldage; 23 _newage = newage; 24 } 25 } 코드 10 이제 Person 클래스는이렇게변경됩니다. 01 public event AgeChangedEventHandler AgeChanged; 02 03 public void IncreaseAge() 04 { 05 int oldage = _age; 06 _age++; 07 08 if (AgeChanged!= null) 09 AgeChanged(this, new AgeChangedEventArgs(oldAge, _age)); 10 } 코드 11 Person 클래스를사용하는쪽의코드는이런형태가되겠지요. 01 static void Main(string[] args) 03 Person person = new Person(32, " 김과장 "); 04 person.agechanged += Person_AgeChanged; 05 06 person.increaseage(); 07 } 7
08 09 private static void Person_AgeChanged(object sender, AgeChangedEventArgs e) 10 { 11 Console.WriteLine(string.Format("{0} -> {1}", e.oldage, e.newage)); 12 } 코드 12 4 번라인에서이벤트처리기를추가할때, = 연산자가아닌 += 연산자를사용하고 있습니다. 이벤트에대해서는 = 연산을수행할수없습니다. 실행결과는대리자를사용하는경우와동일합니다. 32 - > 33 계속하려면아무키나누르십시오... 한편, C# 2.0 의클래스라이브러리에는제네릭버전의 EventHandler 대리자가 정의되어있습니다. 이제네릭 EventHandler 를사용하면 AgeChangedEventHandler 는 따로작성할필요가없습니다. 즉, 코드 10 의 1 번라인은삭제가가능한데, 그러면코드 11 는아래와같이고칠수있습니다. 01 public event EventHandler<AgeChangedEventArgs> AgeChanged; 02 03 public void IncreaseAge() 04 { 05 int oldage = _age; 06 _age++; 07 08 if (AgeChanged!= null) 09 AgeChanged(this, new AgeChangedEventArgs(oldAge, _age)); 10 } 코드 13 8
a C# 코딩연습 - 대리자와이벤트 1 번라인에서 AgeChangedEventHandler 대신 EventHandler<AgeChangedEventArgs> 를사용하고있습니다. 이때, Person 클래스를 사용하는코드 12 은수정할필요가없습니다. D. 상속과이벤트 1. Template Method 패턴 이번에는연예인을나타내는클래스 Entertainer 를작성하는경우를생각해봅시다. 물론연예인은사람이니까 (is 작성하면좋겠지요. 관계 ) Entertainer 는 Person 을상속받는형태로 1 public class Entertainer : Person 2 { 3 public Entertainer(int age, string name) : base(age, name) 4 { 5 } 6 } 코드 14 Entertainer 는기본적으로 Person 과동일하지만, 나이가한살더해졌을때 Person 과는달리이를외부에거짓으로알려준다고가정을해볼까요. 즉 Entertainer 의 IncreaseAge 메소드가아래처럼구현된다고생각합시다. 1 public void IncreaseAge() 2 { 3 int oldage = _age; 4 _age++; 5 6 if (AgeChanged!= null) 7 AgeChanged(this, new AgeChangedEventArgs(oldAge, oldage)); 8 } 코드 15 9
4 번라인에서보는것처럼물론연예인도나이가더해지는건맞습니다. 하지만이를 외부에알려줄때는더해진나이대신에예전나이를알려줍니다. 그래서 7 번라인에서 newage 가들어갈자리에 oldage 를넘기고있는것입니다. 물론현재 IncreaseAge 메서드는 Person 과 Entertainer 에서중복정의되어있습니다. 중복문제를해결하기위해서는 (1) Entertainer.IncreaseAge 에 new 한정자를붙이거나, (2) Person.IncreaseAge 를가상메서드로만들고 Entertainer.IncreaseAge 가이를재정의 ( 오버라이드 ) 하는두가지방법중하나를선택할수있습니다. 나중에 Entertainer 클래스를상속받는 FemaleEntertainer 와같은클래스가또생길수있는가능성을염두에두면가상메서드의재정의를사용하는것이좀더유연하겠지요. Person.IncreaseAge 메서드를가상으로만들고 Entertainer.IncreaseAge 가이를 재정의하는코드는아래와같습니다. 1 public virtual void IncreaseAge() 2 { 3 int oldage = _age; 4 _age++; 5 6 if (AgeChanged!= null) 7 AgeChanged(this, new AgeChangedEventArgs(oldAge, _age)); 8 } 코드 16 Person 의 IncreaseAge 메서드 1 public override void IncreaseAge() 2 { 3 int oldage = _age; 4 _age++; 5 6 if (AgeChanged!= null) 7 AgeChanged(this, new AgeChangedEventArgs(oldAge, oldage)); 8 } 코드 17 Entertainer 의 IncreaseAge 메서드 10
하지만가상메서드를재정의했음에도불구하고, 코드 16 과코드 17 은여전히몇가지 문제를가지고있습니다. _age 변수는 Person 의 private 필드이므로 Entertainer 클래스에서접근할수없습니다. 3 ~ 4 번라인이두클래스에서중복되어있습니다. 파생클래스에서부모클래스에정의된이벤트에대해서수행할수있는연산은 += 와 -= 밖에없습니다. 즉코드 17 의 6 ~ 7 번라인은컴파일에러입니다. 결국 IncreaseAge 메서드를재정의해서사용하는방법으로는문제를해결할수없을것같습니다. 대신다른방법을생각해보아야할것인데요. 위코드 16 과코드 17 을유심히보면, 두코드에서차이가나는부분은 7 번라인밖에없습니다. IncreaseAge 메서드는 Person 클래스에서한번만정의하고, 두코드에서서로다르게동작하는 7 번라인에해당하는부분을새로운메서드로만들어서, IncreaseAge 메서드가새로운메서드를호출하는로직으로바꾸면될것같습니다. 이때, 이새로운메서드는 Person 과 Entertainer 에서서로다르게동작을해야하므로가상메서드로만들어야합니다. 이러한패턴을바로 Template Method 패턴이라고합니다. 코드를살펴보면서이야기를해보지요. Person 클래스의코드부터봅시다. 01 protected virtual void OnAgeChanged(AgeChangedEventArgs e) 03 if (AgeChanged!= null) 04 AgeChanged(this, e); 05 } 06 07 public void IncreaseAge() 08 { 09 int oldage = _age; 10 _age++; 11
11 12 OnAgeChanged(new AgeChangedEventArgs(oldAge, _age)); 13 } 코드 18 이벤트를호출하는부분을따로빼서 OnAgeChanged 라는메서드를새로만들었습니다. 이메서드는 Entertainer 에서재정의할것이기때문에가상메서드로선언하였습니다. 그리고 IncreaseAge 메서드는이 OnAgeChanged 메서드를호출하고있습니다. IncreaseAge 가더이상가상메서드가아닌점도확인하여주십시오. Entertainer 클래스의코드도보지요. 1 protected override void OnAgeChanged(AgeChangedEventArgs e) 2 { 3 base.onagechanged(new AgeChangedEventArgs(e.OldAge, e.oldage)); 4 } 코드 19 Entertainer 클래스에서는이제 IncreaseAge 메서드가없습니다. 대신 OnAgeChanged 메서드를재정의하는코드가추가되었습니다. 재정의된 OnAgeChanged 메서드는 Person 의 OnAgeChanged 를호출합니다. 이때 OnAgeChanged 의매개변수를적절히조작하여전달을합니다. 우리의예에서는새나이대신에예전나이를넘기고있습니다. 만일연예인의경우, 나이가더해졌을때새나이대신에예전나이를알려주는것이아니라아예나이를알려주지않는다고한다면, 코드 19 에서 3 번라인을삭제해버리면될것입니다. 이처럼가상메서드를재정의해서사용하면대단히유연한제어를할수가있습니다. Person 과 Entertainer 를사용하는코드는아마도이런식이겠지요. 12
01 static void Main(string[] args) 03 Person person = new Person(32, " 김과장 "); 04 person.agechanged += Person_AgeChanged; 05 person.increaseage(); 06 07 Person entertainer = new Entertainer(32, " 김스타 "); 08 entertainer.agechanged += Person_AgeChanged; 09 entertainer.increaseage(); 10 } 11 12 private static void Person_AgeChanged(object sender, AgeChangedEventArgs e) 13 { 14 Person person = sender as Person; 15 if (person!= null) 16 { 17 Console.WriteLine(string.Format("{0} : {1} -> {2}", person.name, e.oldage, e.newage)); 18 } 19 } 코드 20 3 번라인과 7 번라인에서각각 Person 과 Entertainer 의객체를하나씩생성하였습니다. 그리고두객체의 AgeChanged 이벤트에모두 Person_AgeChanged 이벤트핸들러를등록하였습니다. 또한 14 번라인에서확인할수있듯이, Person_AgeChanged 이벤트핸들러의 sender 매개변수에는이벤트를발생시킨객체가넘어옵니다. 위예에서는 person 과 entertainer 객체가각각넘어오겠네요. 왜냐하면코드 18 의 4 번라인의 AgeChanged(this, e); 라는코드에의해 Person_AgeChanged 이벤트핸들러가실행되는데, 이때 this 객체는 Person 객체 ( 혹은 Person 에서상속받은 Entertainer 객체 ) 이기때문입니다. 실행결과는다음과같습니다. 김과장 : 32 - > 33 김스타 : 32 - > 32 13
계속하려면아무키나누르십시오... 예상한대로연예인인김스타는민간인이김과장과는달리, 32 살에서 33 살이되어도 여전히 32 살이라고외부에알려주고있습니다. 여기까지의 Person 과 Entertainer 의전체코드는다음과같습니다. 01 public class AgeChangedEventArgs : EventArgs 03 private int _oldage; 04 private int _newage; 05 06 public int OldAge 07 { 08 get { return _oldage; } 09 set { _oldage = value; } 10 } 11 12 public int NewAge 13 { 14 get { return _newage; } 15 set { _newage = value; } 16 } 17 18 public AgeChangedEventArgs(int oldage, int newage) 19 { 20 _oldage = oldage; 21 _newage = newage; 22 } 23 } 코드 21 AgeChangedEventArgs 클래스 01 public class Person 03 private int _age; 04 05 public int Age 06 { 07 get { return _age; } 08 set { _age = value; } 09 } 10 11 private string _name; 12 14
13 public string Name 14 { 15 get { return _name; } 16 set { _name = value; } 17 } 18 19 public Person(int age, string name) 20 { 21 _age = age; 22 _name = name; 23 } 24 25 public event EventHandler<AgeChangedEventArgs> AgeChanged; 26 27 protected virtual void OnAgeChanged(AgeChangedEventArgs e) 28 { 29 if (AgeChanged!= null) 30 AgeChanged(this, e); 31 } 32 33 public void IncreaseAge() 34 { 35 int oldage = _age; 36 _age++; 37 38 OnAgeChanged(new AgeChangedEventArgs(oldAge, _age)); 39 } 40 } 코드 22 Person 클래스 01 public class Entertainer : Person 03 public Entertainer(int age, string name) : base(age, name) 04 { 05 } 06 07 protected override void OnAgeChanged(AgeChangedEventArgs e) 08 { 09 base.onagechanged(new AgeChangedEventArgs(e.OldAge, e.oldage)); 10 } 11 } 코드 23 Entertainer 클래스 2. 상속된윈폼의이벤트 상속과이벤트에대해서또한가지생각해볼문제가있습니다. 15
윈도우폼응용프로그램이 BaseForm 과 DerivedForm 이라는두개의윈폼클래스를 가지고있다고합시다. BaseForm 은 System.Windows.Forms.Form 을상속받으며 DerivedForm 은이 BaseForm 을상속받는형태입니다. 이때, 두윈폼클래스의 Load 이벤트가발생할때어떤작업, 여기서는간단히메시지 박스를띄우는일을해보겠습니다. 먼저 BaseForm 의 Load 이벤트에이벤트핸들러를 연결합니다. 01 public partial class BaseForm : Form 03 public BaseForm() 04 { 05 InitializeComponent(); 06 07 Load += new EventHandler(BaseForm_Load); 08 } 09 10 private void BaseForm_Load(object sender, EventArgs e) 11 { 12 MessageBox.Show("BaseForm Loaded"); 13 } 14 } 코드 24 그다음에는 DerivedForm 의 Load 이벤트에대해서도동일한작업을합니다. 01 public partial class DerivedForm : BaseForm 03 public DerivedForm() 04 { 05 InitializeComponent(); 06 07 Load += new EventHandler(DerivedForm_Load); 08 } 09 10 private void DerivedForm_Load(object sender, EventArgs e) 11 { 12 MessageBox.Show("DerivedForm Loaded"); 13 } 14 } 16
코드 25 코드 24 와코드 25 의 7 번라인을보면 Form 클래스의 Load 이벤트에각각이벤트핸들러를등록하고있습니다. 즉 Load 이벤트에는 BaseForm_Load 와 DerivedForm_Load 라는두개의이벤트핸들러가등록되어있습니다. 따라서 DerivedForm 이로드되면 BaseForm_Load 이벤트핸들러와 DerivedForm_Load 이벤트핸들러가순서대로실행됩니다. 그런데, 만일이두이벤트핸들러를이순서대로실행하는것이우리가의도하는 바가아니라면어떻게할까요? 예컨데 DerivedForm 이로드될때 BaseForm_Load 는 실행하지않고 DerivedForm_Load 만실행하려고한다면요? 이러한문제를해결하기위해서는 Load 이벤트가정의된클래스 (Form) 의자식클래스들 (BaseForm, DerivedForm) 에서 Load 이벤트에바로이벤트핸들러를등록하여서는안됩니다. 대신앞에서이야기한것처럼, Form 클래스의메서드중 Load 이벤트를호출하는메서드를찾아이를 BaseForm 과 DerivedForm 에서재정의하여야합니다. Form 클래스는이미이러한용도로사용하기위해 OnLoad 라는가상메서드를 제공하고있습니다. OnLoad 메서드의구현은아마도다음과같은형태일것입니다. 1 protected virtual void OnLoad(EventArgs e) 2 { 3 if (Load!= null) 4 Load(this, EventArgs.Empty); 5 } 코드 26 이 OnLoad 메서드를재정하는 BaseForm 의코드는다음과같습니다. 17
01 public partial class BaseForm : Form 03 public BaseForm() 04 { 05 InitializeComponent(); 06 } 07 08 protected override void OnLoad(EventArgs e) 09 { 10 MessageBox.Show("BaseForm Loaded"); 11 } 12 } 코드 27 코드에서드러나지는않지만, 코드 24 의 7 번라인과같이 Load += new EventHandler(BaseForm_Load); 와같은코드는이제존재하지않습니다. 즉, BaseForm 은 Form 의 Load 이벤트을사용하지않습니다. 하지만 Load 이벤트에이벤트핸들러를등록한것과동일하게동작을합니다. 어떻게이것이가능할까요? Form 클래스의코드중에, 자신이로드가되고나면 OnLoad 메서드를호출하는코드가있을것입니다. 그런데이 OnLoad 메서드는가상메서드이므로 BaseForm 클래스에서이를재정의해버리면, 결국 Form 클래스는자신이로드되고나면자신의 OnLoad 메서드를호출하는것이아니라 BaseForm 의 OnLoad 메서드를호출하게되는것입니다. 재미있는코드를한번볼까요. 아래코드를보고결과를예상해보십시오. 01 public partial class BaseForm : Form 03 public BaseForm() 04 { 05 InitializeComponent(); 06 07 Load += BaseForm_Load; 08 } 09 18
10 void BaseForm_Load(object sender, EventArgs e) 11 { 12 MessageBox.Show("BaseForm Loaded By Event"); 13 } 14 15 protected override void OnLoad(EventArgs e) 16 { 17 // base.onload(e); 18 19 MessageBox.Show("BaseForm Loaded"); 20 } 21 } 코드 28 Load 이벤트에이벤트핸들러도등록하고, 동시에 OnLoad 메서드도재정의 하였습니다. 그렇다면 12 번라인과 19 번라인의메시지박스는둘다나타날까요? BaseForm 이로드가되었을때, BaseForm 이상속받은 Form 의어떤코드에의해서 OnLoad 메서드가실행이됩니다. 그런데이 OnLoad 메서드는가상메서드이기때문에, Form 의것이아니라 BaseForm 의 OnLoad 가실행됩니다. 그것이동작의전부입니다. Load 이벤트에등록된이벤트핸들러들을실행하는코드는 Form 의 OnLoad 에있는데, 이 Form 의 OnLoad 는 BaseForm 의 OnLoad 에의해가리워져실행이되지않는것입니다. 따라서 12 번의메시지박스는나타나지않습니다. 만일 17 번라인의주석을풀고실행을하면어떤결과가나타날까요? 12 번의메시지박스가먼저나타나고그다음에 19 번의메시지박스가나타난다 고설명을할수있다면, 원래의목적이었던 BaseForm 과 DerivedForm 의 Load 이벤트들을제어하는것은쉽게하실수있을것입니다. 여기서는확인삼아 DerivedForm 의메시지박스를먼저띄우고그다음에 BaseForm 의메시지박스를띄우는코드를적겠습니다. 생각한코드가맞는지확인하시기바랍니다. 19
01 public partial class BaseForm : Form 03 public BaseForm() 04 { 05 InitializeComponent(); 06 } 07 08 protected override void OnLoad(EventArgs e) 09 { 10 MessageBox.Show("BaseForm Loaded"); 11 } 12 } 코드 29 BaseForm 01 public partial class DerivedForm : BaseForm 03 public DerivedForm() 04 { 05 InitializeComponent(); 06 } 07 08 protected override void OnLoad(EventArgs e) 09 { 10 MessageBox.Show("DerivedForm Loaded"); 11 12 base.onload(e); 13 } 14 } 코드 30 DerivedForm E. 이벤트코드조각생성기 이때까지의이야기를종합하여이벤트를추가하는과정을정리하면다음과같습니다. 1. 이벤트에서사용할대리자를작성한다. 또는제네릭 EventHandler 를사용한다. 2. EventArgs 를상속하는이벤트인자클래스를작성한다. 3. 이벤트를선언한다. 4. 이벤트를호출하는가상메서드를작성한다. 20
각과정에따라이벤트관련코드를작성하다보면반복적인타이핑작업이일어남을알수있습니다. 그렇다면이런기계적이고반복적인코드를자동으로생성할수도있을것입니다. 그래서간단한툴을만들어보았습니다. 특별한이름을짓지못해그냥이벤트코드조각생성기라고부르겠습니다. 그림 1 이벤트코드조각생성기 1. 사용방법 사용방법은간단합니다. Event Name 에생성할이벤트의이름을입력하고, Event Arguments 에매개변수들을입력하면됩니다. 그림과같이각매개변수는개행으로구분하고매개변수의형과이름은띄어쓰기로구분을합니다. Generate 버튼을누르면오른편에있는두개의텍스트박스에생성된코드들이출력됩니다. 이를복사하여사용하면되겠습니다. 21
생성된코드가출력되는텍스트박스를두개로나눈이유는생성되는두개의코드조각이복사될위치가다르기때문입니다. 위쪽텍스트박스의경우이벤트의선언과이벤트를호출하는가상메서드가생성되는데, 이는클래스내부에들어갈코드조각입니다. 반면에아래쪽텍스트박스에는이벤트인자클래스가생성되는데, 이는이벤트클래스의외부에두는것이권장되는구조입니다. 또한 Copy on click 체크박스가클릭되어있는상태에서오른쪽의생성된코드를 클릭하면생성된코드가클립보드로바로복사가됩니다. 2. 템플릿 이벤트코드조각생성기의핵심로직은간단합니다. 생성될코드조각의뼈대가되는 템플릿을사용자가입력한이벤트이름과이벤트인자를사용하여치환하는것이거든요. 예를들어 Event Snippet 텍스트박스에서사용되는템플릿의내용은다음과같습니다. 매크로 의미 예 {0} 이벤트이름 AgeChanged {1} 이벤트인자클래스 AgeChangedEventArgs {2} 매개변수형과이름목록 int oldage, int newage {3} 매개변수이름목록 oldage, newage ### 이하템플릿시작 #region {0} event public event EventHandler<{1}> {0}; protected virtual void On{0}({1} e) {{ if({0}!= null) {0}(this, e); }} protected virtual void On{0}({2}) {{ if({0}!= null) 22
}} #endregion {0}(this, new {1}({3})); 필요한경우이템플릿파일을수정하면생성되는코드를제어할수있습니다. 이벤트코드조각생성기는두개의템플릿을사용합니다. (EventSnippet.txt, ArgumentSnippet.txt) 자세한사항은이벤트코드조각생성기의소스를참고하시기 바랍니다. 3. 데모 이벤트코드조각생성기를사용하여 Person 클래스에 AgeChanged 이벤트를 추가하는과정을살펴보겠습니다. 먼저이벤트코드조각생성기를실행시키고, 이벤트 이름과이벤트인자를각각다음과같이입력합니다. 이벤트이름 AgeChanged 이벤트인자 int oldage int newage Generate 버턴을누르면이벤트코드조각이생성됩니다. 먼저 Event Snippet 의 내용을클릭하여복사한후 Person 클래스에붙여넣습니다. 01 public class Person 03... 04 #region AgeChanged event 05 public event EventHandler<AgeChangedEventArgs> AgeChanged; 06 07 protected virtual void OnAgeChanged(AgeChangedEventArgs e) 08 { 09 if (AgeChanged!= null) 10 AgeChanged(this, e); 11 } 23
12 13 protected virtual void OnAgeChanged(int oldage, int newage) 14 { 15 if (AgeChanged!= null) 16 AgeChanged(this, new AgeChangedEventArgs(oldAge, newage)); 17 } 18 #endregion 19 20 public void IncreaseAge() 21 { 22 int oldage = _age; 23 _age++; 24 25 OnAgeChanged(oldAge, _age); 26 } 27 } 코드 31 13 번라인의메서드는이전예제에서는등장하지않았던메서드입니다. 이는단지 코딩의편리함을위해원래의 OnAgeChanged 메서드를래퍼하는것에불과합니다. 25 번라인역시이새로운 OnAgeChanged 메서드를호출하는형태로변경되었습니다. 이번에는 Arguments Snippet 의코드를복사하여 Person 클래스의외부에붙여넣습니다. 물론 AgeChangedEventArgs 클래스를 Person 클래스내부에포함된클래스로위치시킬수도있겠지만권장되는설계는아닙니다. AgeChangedEventArgs 의코드는코드 21 과동일하므로생략합니다. F. 사용자정의컨트롤의이벤트 이벤트를작성하는좀더실용적인예를생각해봅시다. 윈도우응용프로그램을 개발하는중사용자정의컨트롤을만든다고가정을하지요. 대략다음과같은컨트롤을 만들었습니다. ( 이하이사용자정의컨트롤을 MemoWriter 라고하겠습니다.) 24
그림 2 MemoWriter 사용자정의컨트롤 MemoWriter 는사용자로부터메모를입력받는간단한기능을하는컨트롤입니다. 이 컨트롤을가지고있는폼역시아주단순합니다. ( 이하이윈폼을 Form1 이라고 하겠습니다.) 그림 3 Form1 윈폼클래스 사용자가텍스트박스에메모를입력하고 Write 버튼을클릭하면입력한메모를 메시지박스로띄우는기능을구현하고자합니다. 어떤방법이있을까요? 만일버튼이 Form1 의자식컨트롤이라면단순히버튼의클릭이벤트핸들러를만드는것으로가능합니다. 하지만문제는버튼은 Form1 의자식컨트롤이아니라 MemoWriter 의자식컨트롤입니다. 따라서 Form1 은자신의자식컨트롤이아닌버튼의이벤트에이벤트핸들러를등록할수가없습니다. 이문제를해결하는정석은 MemoWriter 에 Written 이라는이벤트를추가하고, Form1 이이이벤트에대한이벤트핸들러를등록하는것입니다. MemoWriter 안에있는버튼이클릭되었을때, MemoWriter 가 Written 이벤트를발생시키면, Form1 에있는 Written 의이벤트핸들러가실행되는로직이지요. 25
직접코드를보면서이야기하면이해가더쉽겠지요. 먼저 MemoWriter 에 Written 이벤트관련코드를추가합니다. 01 public partial class MemoWriter : UserControl 03 public MemoWriter() 04 { 05 InitializeComponent(); 06 } 07 08 private void btnwrite_click(object sender, EventArgs e) 09 { 10 OnWritten(txtMemo.Text); 11 } 12 13 #region Written event 14 public event EventHandler<WrittenEventArgs> Written; 15 16 protected virtual void OnWritten(WrittenEventArgs e) 17 { 18 if (Written!= null) 19 Written(this, e); 20 } 21 22 protected virtual void OnWritten(string memo) 23 { 24 if (Written!= null) 25 Written(this, new WrittenEventArgs(memo)); 26 } 27 #endregion 28 } 29 30 #region WrittenEventArgs 31 public class WrittenEventArgs : EventArgs 32 { 33 private string _memo; 34 35 public string Memo 36 { 37 get { return _memo; } 38 set { _memo = value; } 39 } 40 41 public WrittenEventArgs(string memo) 42 { 43 _memo = memo; 44 } 26
45 } 46 #endregion 코드 32 8 ~ 11 번라인을주의해서볼필요가있습니다. 버튼에대한이벤트핸들러내에서 Written 이벤트를다시발생시키고있습니다. ( 물론 btnwrite.clicked 이벤트에 btnwrite_click 이벤트핸들러를등록하는코드는생략되어있습니다.) Form1 의코드는아래와같습니다. 01 public partial class Form1 : Form 03 public Form1() 04 { 05 InitializeComponent(); 06 } 07 08 private void uscmemowriter_written(object sender, WrittenEventArgs e) 09 { 10 MessageBox.Show(e.Memo); 11 } 12 } 코드 33 MemoWriter 의 Written 이벤트에이벤트핸들러를등록하였네요. 또한 10 번라인에서 WrittenEventArgs 의 Memo 프로퍼티를통해사용자가텍스트박스에입력한메모내용을읽을수있는것도알수있습니다. ( 물론코드 33 에서도 MemoWriter 의인스턴스인 usc MemoWriter 의 Written 이벤트에 uscmemowriter_written 이벤트핸들러를등록하는코드는생략되었습니다.) 27