예전에는 WPF 개발을 MVVM 구조로 진행해서 ViewModel과 Model을 사용해서 데이터를 전달하곤 했는데, 이번에는 개발 디자인 패턴이 바뀌어서 MVC로 진행하게 되었습니다.
데이터 전달을 EventHandler라는 것을 사용하게 되는데 간단히 정리해보도록 하겠습니다.
1. EventHander 선언
public event EventHandler<string> TitleSelected;
// string 타입의 데이터를 이벤트 핸들러에 전달할 수 있음
2. EventHanlder가 사용될 이벤트 등록 및 구현
lv_Presents.SelectionChanged += Lv_Presents_SelectionChanged;
// ListView에서 항목이 변경될 때 실행되는 이벤트(Lv_Presents_SelectionChanged)를 등록
// 선택 항목이 바뀔 때마다 메서드가 호출
private void lv_Presents_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (lv_Presents.SelectedItem is PresentModel model)
{
TitleSelected?.Invoke(this, model.title);
// this: 현재 UserControl을 이벤트 발신자로 지정
// model.title: 전달할 데이터
// ?.Invoke: 이벤트 구독자에게 데이터 전달(구독자가 없을 때 예외가 발생하지 않음)
}
}
3. 이벤트 구독 및 처리로직 구현
uc_Presents.TitleSelected += uc_Presents_TitleSelected;
// uc_Presents TitleSelected 이벤트가 발생하면, uc_Presents_TitleSelected 메서드 실행
// Window가 생성될 때 이벤트 핸들러를 등록하여 UserControl에서 발생한 이벤트를 받을 준비를 함
private void uc_Presents_TitleSelected(object sender, string title)
{
tblock_SelectedTitle.Text = title;
}
💡 이벤트 해제
* 객체가 더 이상 사용되지 않거나 이벤트를 더 이상 받을 필요가 없을 때는 -=로 해제하는 것이 좋음
* 이벤트 핸들러가 해제되지 않으면 구독자가 GC에 의해 수거되지 않기 때문에, 메모리 누수가 발생할 수 있음
* 특정 IP 주소와 포트에서 클라이언트의 연결 요청을 기다리고, 연결이 수립되면 데이터를 주고받는 역할
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TcpServerTest1
{
internal class Program
{
static void Main(string[] args)
{
NetworkStream stream = null;
TcpListener listener = null;
Socket clientSocket = null;
StreamReader reader = null;
StreamWriter writer = null;
try
{
// 1. 서버 IP 주소 설정
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
// 2. TCPListener 생성 및 시작
// TCPListener: 클라이언트의 연결을 수신
listener = new TcpListener(ipAddress, 5001);
listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작
// 3. 클라이언트의 연결 대기
// 클라이언트의 연결 요청을 받아들여 Socket 객체를 반환
// 클라이언트가 연결을 시도할 때까지 블로킹(즉, 이 코드에서 멈추고 기다림)
clientSocket = listener.AcceptSocket();
// 4. 클라이언트와의 데이터 통신을 위한 스트림 설정
// clientSocket과 연결된 NetworkStream 객체를 생성 -> 이를 통해 클라이언트와 데이터를 주고받을 수 있음
// Encoding: 데이터를 바이트로 변환하거나 바이트를 텍스트로 변환할 때 사용
stream = new NetworkStream(clientSocket);
Encoding encoding = Encoding.GetEncoding("utf-8");
// 5. 데이터 읽기와 쓰기를 위한 스트림 리더와 라이터 생성
reader = new StreamReader(stream, encoding);
writer = new StreamWriter(stream, encoding) { AutoFlush = true };
// 6. 데이터 수신 및 송신 루프
while (true)
{
// 클라이언트로부터 데이터 읽기
string str = reader.ReadLine();
Console.WriteLine(str);
// 클라이언트에게 데이터 응답
writer.WriteLine(str);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
clientSocket.Close();
}
}
}
}
TCP Client
using System.Net.Sockets;
using System.Text;
namespace TcpClientTest1
{
internal class Program
{
static void Main(string[] args)
{
TcpClient client = null;
try
{
// 1. TCPClient 생성
client = new TcpClient();
// 2. 서버에 연결 시도
// 서버가 해당 포트에서 연결 요청을 듣고 있어야 함
client.Connect("localhost", 5001);
// 3. 네트워크 스트림 획득
// 네트워크 상에서 데이터를 스트림 방식으로 송수신하는 기능을 제공
// 스트림: 데이터를 바이트 단위로 처리하며, 데이터를 차례로 읽거나 쓸 수 있는 방식
// 예를 들어, 서버가 클라이언트에 메시지를 보낼 때 NetworkStream을 통해 데이터를 읽을 수 있음
// 클라이언트는 NetworkStream을 통해 서버로 메시지를 전송할 수 있음
NetworkStream stream = client.GetStream();
// 4. 인코딩 설정
Encoding encoding = Encoding.GetEncoding("utf-8");
// 5. 스트림 리더와 라이터 생성
// StreamWriter: 데이터를 서버로 보내는 데 사용, AutoFlush: 매번 쓰기 작업 후에 버퍼를 자동으로 비움
// StreamReader: 서버로부터 데이터를 읽는 데 사용
StreamWriter writer = new StreamWriter(stream) { AutoFlush=true };
StreamReader reader = new StreamReader(stream, encoding);
// 6. 사용자 입력 받기
string dataToSend = Console.ReadLine();
while (true)
{
// 7. 서버에 데이터 전송
writer.WriteLine(dataToSend);
// 8. 종료 조건 체크
if (dataToSend.IndexOf("<EOF>") > -1)
{
break;
}
// 9. 서버로부터 데이터 읽기 및 출력
Console.WriteLine(reader.ReadLine());
// 10. 다음 데이터 입력
dataToSend = Console.ReadLine();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
throw;
}
finally
{
client.Close();
}
}
}
}
TCP Server + Multi Threading
* 기존 TCP Server에서는 Server : Client가 1:1 구조를 이루면서, 클라이언트가 2이상일 때 처리하지 못함
* 여러 클라이언트가 동시에 서버에 연결하고 통신할 수 있도록 멀티스레딩을 활용
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TcpServerTest1
{
// 개별 클라이언트와의 통신을 처리
// 각 클라이언트는 별도의 ClientHandler 인스턴스를 가지며, 이 인스턴스는 별도의 스레드에서 실행
internal class ClientHandler
{
// 클라이언트와의 연결을 나타내는 소켓
Socket socket = null;
// 클라이언트와의 데이터 통신을 위한 스트림
NetworkStream stream = null;
// 클라이언트로부터 데이터를 읽는 리더
StreamReader reader = null;
// 클라이언트에게 데이터를 쓰는 라이터
StreamWriter writer = null;
public ClientHandler(Socket socket)
{
this.socket = socket;
}
// 클라이언트와 데이터를 주고받는 메서드
// 무한 루프를 사용하여 지속적으로 클라이언트로부터 데이터를 읽고 응답
public void Chat()
{
stream = new NetworkStream(socket);
Encoding encoding = Encoding.GetEncoding("utf-8");
reader = new StreamReader(stream, encoding);
writer = new StreamWriter(stream, encoding) { AutoFlush = true };
while (true)
{
// 클라이언트로부터 데이터 읽기
string str = reader.ReadLine();
Console.WriteLine(str);
// 클라이언트에게 데이터 응답
writer.WriteLine(str);
}
}
}
class Program
{
static void Main(string[] args)
{
TcpListener listener = null;
Socket clientSocket = null;
try
{
// 1. 서버 IP 주소 설정
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
// 2. TCPListener 생성 및 시작
// TCPListener: 클라이언트의 연결을 수신
listener = new TcpListener(ipAddress, 5001);
listener.Start(); // 서버가 클라이언트의 연결 요청을 수신하도록 시작
while (true)
{
// 프로그램이 클라이언트의 연결 요청을 받을 때마다, 새로운 Thread를 생성하고 그 스레드가 클라이언트와의 통신을 처리
// 3. 클라이언트의 연결 대기 및 수락
clientSocket = listener.AcceptSocket();
// 4. 클라이언트 처리기 생성 및 스레드 시작
ClientHandler handler = new ClientHandler(clientSocket);
Thread t = new Thread(new ThreadStart(handler.Chat)); // 새 스레드를 시작하여 클라이언트와의 통신을 독립적으로 처리
t.Start();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
clientSocket.Close();
}
}
}
}
새로운 Template을 만들어 사용할 경우, 내용(Content)을 표시하는 기능인 ContentPresenter도 함께 선언해야 합니다.
3. TemplateBinding
ControlTemplate 안에서 외부의 속성 값을 바인딩할 때 사용합니다.
여기서는 DefaultButton을 사용한 Button의 Background인 DodgerBlue를 참조하게 됩니다.
4. Trigger
마지막으로 trigger는 특정 조건이 만족될 때(여기서는 마우스가 버튼 위에 올라왔을 때) 스타일이나 속성을 변경합니다. 이를 통해 ControlTemplate에서 설정된 속성들을 변경할 수 있습니다.
1번 코드와 2번 코드는 Button 안의 TextBlock에 FontWeight를 직접 설정했느냐의 차이가 있습니다.
1번은
Complate.Trigger → Button 속성 변경 → ContentPresenter: 변경된 속성을 Button의 자식 요소인 TextBlock에 전달합니다.
2번은
Complate.Trigger→ Button 속성 변경→ ContentPresenter: 변경된 속성을 Button의 자식 요소인 TextBlock에 전달합니다. 하지만, 자식 요소에서 직접 설정된 속성은 ControlTemplate의 외부에서 설정된 것으로 간주되어, ControlTemplate의 트리거에서 영향을 받지 않습니다.
⭐ 간단하게 정리하면, ControlTemplate은 전체적인 Template를 control할 수 있지만, <> 안에서 직접 설정된 요소는 변경할 수 없습니다.