✔️ Serial 통신 프로그램 2에서 구현한 것을 MVVM 패턴으로 변경
✔️ material design 사용
폴더를 Models, ModelViews, Views로 나누고
Models 에는 SerialModel.cs 파일을 생성해서 Serial 통신에 관련한 모든것을 다루게함. ModelViews 에는 ViewModel.cs 파일 생성해서 MainWindow창에서 일어나는 기능들을 분리해서 구현. Java에서 배운 MVC 패턴과 유사하게 각각의 기능들을 따로 분리해 재사용성을 높였다. MainWindow.xaml에는 MVVM 패턴의 적용을 위해서 Binding 을 사용함, 추가적으로 materialDesignToolKit을 이용해서 wpf 화면을 디자인했다
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.IO.Ports;
using System.Collections.Concurrent;
namespace WpfApp_mvvm.Models
{
internal class SerialModel : IDisposable
{
private SerialPort serialPort;
public SerialModel(string portName, int baudRate) {
serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
serialPort.DataReceived += DataReceivedHandler;
}
public event Action<string> DataReceived;
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
string data = serialPort.ReadLine();
DataReceived?.Invoke(data);
}
public void Open()
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
}
public void Close()
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
public void Write(string message)
{
if (serialPort.IsOpen)
{
serialPort.Write(message);
}
}
public void Dispose()
{
Close();
serialPort?.Dispose();
}
}
}
Serial 통신에 사용되는 로직을 Model로 다 분리했다
<Window x:Class="WpfApp_mvvm.Views.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:vm="clr-namespace:WpfApp_mvvm.ViewModels"
🍋d:DataContext="{d:DesignInstance Type=vm:ViewModel, IsDesignTimeCreatable=False}"
mc:Ignorable="d"
Title="MainWindow" Height="250" Width="500">
<!--d: 이후 설정 : 이 디자인 내에서만 적용되서 DataContext가 적용되는것-->
<!--IsDesignTimeCreatable 이걸 사용하면 실행이 되는 로직을 막는다?-->
<!--data Context, window. DataContext 를 사용하는건 지양된다 View 모델의 재사용성을 떨어트린다-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="StartButton" Content="Start" Command="{Binding StartCommand}" Height="30" Width="75" Margin="8" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" IsEnabled="{Binding Path=StartCommand.CanExecute, Mode=OneWay}"/>
<Button x:Name="StopButton" Content="Stop" Command="{Binding StopCommand}" Height="30" Width="75" Margin="8" Grid.Column="3" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" IsEnabled="{Binding Path=StopCommand.CanExecute, Mode=OneWay}"/>
<Button Name="Button1" Command="{Binding SendRandomNumberCommand}" IsEnabled="{Binding IsPortOpen}" Height="30" Width="75" Grid.Column="1" Grid.Row="3" Content="Random Number"/>
<Button Name="Button2" Command="{Binding SendRandomMessageCommand}" IsEnabled="{Binding IsPortOpen}" Height="30" Width="75" Grid.Column="2" Grid.Row="3" Content="Random String"/>
<Button Name="Button3" Command="{Binding SendMessageFromFileCommand}" IsEnabled="{Binding IsPortOpen}" Height="30" Width="75" Grid.Column="3" Grid.Row="3" Content="File Message"/>
<TextBox Name="InputTextBox" Width="80" Height="30" Grid.Column="4" Grid.Row="2" Text="{Binding InputMessage, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Bottom"/>
<Button Name="Button4" Command="{Binding SendMessageCommand}" IsEnabled="{Binding IsPortOpen}" Height="30" Width="75" Grid.Column="4" Grid.Row="3" Content="SendTextBox"/>
<!--Binding이 되는 dependancy 가 있고 종속성을 바꾸는 경우가 있다??-->
</Grid>
</Window>
<Window x:Class="WpfApp_mvvm.Views.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:vm="clr-namespace:WpfApp_mvvm.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="250" Width="500">
🍉<Window.DataContext>
<vm:ViewModel/>
🍉</Window.DataContext>
<Grid>
// ...화면구현 로직
</Grid>
</Window>
🍉이 부분으로 Window.DataContext를 사용했는데
사수님이 설명해주시길, Window를 통해서 DataContext를 사용하게 되면 View의 재사용성이 떨어진다고 했다.
🍋d:DataContext="{d:DesignInstance Type=vm:ViewModel, IsDesignTimeCreatable=False} 이런 식으로 d:를 이용해서 사용해야한다고 하셨음.
XAML에서 디자인 타임 데이터 컨텍스트를 설정하는 방법 : 디자인 타임 데이터 컨텍스트를 설정하면, Visual Studio와 같은 디자이너에서 UI를 디자인할 때, 실제 데이터가 없어도 데이터 바인딩이 어떻게 작동하는지 미리 볼 수 있다.

디자인 타임 런 타임 에 대한걸 더 알아봐야겠다
일단 이해하기로는 디자인 타임은 xaml을 생성하는 과정에서 미리보기(?)화면이 굉장히 느리게 뜨는 편이었는데 그 시간을 말하는것 같고, 런타임은 실제 실행시에 드는 화면에 대한 시간을 나타내는거 같은데 그걸 효율적으로 사용하기위해서 분리하는 것같음.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Windows;
using WpfApp_mvvm.Models;
using System.Collections;
using System.IO;
using System.Windows.Controls;
namespace WpfApp_mvvm.ViewModels
{
class ViewModel : INotifyPropertyChanged //속성 변경을 알린다 , INotifyCollectionChanged
{
[DllImport("kernel32.dll")]
private static extern bool AllocConsole();
private SerialModel serialModel;
private readonly string[] messageArr = { "test1", "test2", "test3", "test4", "test5" };
//private readonly BlockingCollection<string> messageQueue = new BlockingCollection<string>();
//private readonly BlockingCollection<string> receiveQueue = new BlockingCollection<string>();
private ConcurrentQueue<string> messageQueue = new ConcurrentQueue<string>();
private ConcurrentQueue<string> receiveQueue = new ConcurrentQueue<string>();
public ViewModel()
{
serialModel = new SerialModel("COM4", 115200);
serialModel.DataReceived += DataReceivedHandler;
Task.Run(() => SendMessages());
Task.Run(() => ReceiveMessages());
StartCommand = new RelayCommand(Start, () => !IsPortOpen); //RelayCommend : ICommand interface 구현하는 클래스
//RelayCommand(명령이 실행될때 호출되는 메서드, 명령이 실행가능한지 여부 결정하는 조건)
//클래스의 속성(Property) 또는 명령(Command)을 초기화하는 부분 MVVM 패턴에서 사용하는 일반적인방법
StopCommand = new RelayCommand(Stop, () => IsPortOpen);
SendRandomNumberCommand = new RelayCommand(() => messageQueue.Enqueue("RandomNumber"), () => ButtonIsEnabled);
SendRandomMessageCommand = new RelayCommand(() => messageQueue.Enqueue("RandomMessage"), () => ButtonIsEnabled);
SendMessageFromFileCommand = new RelayCommand(() => messageQueue.Enqueue("MessageFromFile"), () => ButtonIsEnabled);
SendMessageCommand = new RelayCommand(() => messageQueue.Enqueue(InputMessage), () => ButtonIsEnabled);
}
public ICommand StartCommand { get; } //interface : xaml 바인딩을 통해서 UI요소와 연결
public ICommand StopCommand { get; }
public ICommand SendRandomNumberCommand { get; }
public ICommand SendRandomMessageCommand { get; }
public ICommand SendMessageFromFileCommand { get; }
public ICommand SendMessageCommand { get; }
private bool isPortOpen; //캡슐화
public bool IsPortOpen
{
get => isPortOpen;
set
{
isPortOpen = value;
OnPropertyChanged(nameof(IsPortOpen));
CommandManager.InvalidateRequerySuggested();
}
}
private string inputMessage;
public string InputMessage
{
get => inputMessage;
set
{
inputMessage = value;
OnPropertyChanged(nameof(InputMessage));
}
}
private bool buttonIsEnabled = false;
public bool ButtonIsEnabled {
get => buttonIsEnabled;
set
{
buttonIsEnabled = value;
OnPropertyChanged(nameof(ButtonIsEnabled));
CommandManager.InvalidateRequerySuggested();
}
}
private void DataReceivedHandler(string data)
{
receiveQueue.Enqueue(data);
}
private void Start()
{
try
{
serialModel.Open();
AllocConsole();
IsPortOpen = true;
ButtonIsEnabled = true;
OnPropertyChanged(nameof(ButtonIsEnabled));
Console.WriteLine($"{DateTime.Now}: Serial port Open");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"{ex.Message}");
}
}
private void Stop()
{
serialModel.Close();
IsPortOpen = false;
ButtonIsEnabled = false;
OnPropertyChanged(nameof(ButtonIsEnabled));
Console.WriteLine($"{DateTime.Now}: Serial port Close");
}
private void SendMessages()
{
while (true)
{
try //finally 문까지
{
if (messageQueue.TryDequeue(out var message))
{
Application.Current.Dispatcher.Invoke(() =>
{
Console.WriteLine($"{DateTime.Now}: {message} sent ");
});
switch (message)
{
case "RandomNumber":
SendRandomNumber();
break;
case "RandomMessage":
SendRandomMessage();
break;
case "MessageFromFile":
SendMessageFromFile();
break;
default:
SendMessageFromTextBox(message);
break;
}
}
else
{
Task.Delay(100).Wait(); // 메시지가 없을 때 잠시 대기
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in SendMessages: {ex.Message}");
}
//finally : 예외의 발생 여부와 상관없이 항상 실행
}
}
private void ReceiveMessages()
{
while (true)
{
try
{
if (receiveQueue.TryDequeue(out var message))
{
Application.Current.Dispatcher.Invoke(() =>
{
Console.WriteLine($"{DateTime.Now}: {message} received ");
});
if (message.Contains("$"))
{
Application.Current.Dispatcher.Invoke(() =>
{
Console.WriteLine("Special command handling executed.");
});
}
}
else
{
Task.Delay(100).Wait(); // 메시지가 없을 때 잠시 대기
}
}
catch (Exception ex) {
Console.WriteLine($"Error in ReceiveMessages: {ex.Message}");
}
}
}
private void SendRandomNumber()
{
Random random = new Random();
int nums = random.Next(0, 100);
string message = $"${nums}\r\n";
serialModel.Write(message);
}
private void SendRandomMessage()
{
Random random = new Random();
string message = $"${messageArr[random.Next(messageArr.Length)]}\r\n";
serialModel.Write(message);
}
private void SendMessageFromFile()
{
try
{
string message = File.ReadAllText("message.txt");
message = $"${message}\r\n";
serialModel.Write(message);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"{ex.Message}");
}
}
private void SendMessageFromTextBox(string message)
{
message = $"${message}\r\n";
serialModel.Write(message);
}
public event PropertyChangedEventHandler PropertyChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class RelayCommand : ICommand //이거 돌아가는거 이해하기
{
private readonly Action execute;
private readonly Func<bool> canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter) => canExecute == null || canExecute();
public void Execute(object parameter) => execute();
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
}
private bool isPortOpen; //캡슐화!
public bool IsPortOpen
{
get => isPortOpen;
set
{
isPortOpen = value;
OnPropertyChanged(nameof(IsPortOpen));
CommandManager.InvalidateRequerySuggested();
}
}
OnPropertyChanged(nameof(IsPortOpen)); : 속성 값이 변경되었음을 알리는 메서드를 호출. 이는 일반적으로 데이터 바인딩 시 UI 업데이트에 사용된다.
사수분께서 내 코드를 봐주실때 제일 먼저 하셨던 질문이 isPortOpen와 IsPortOpen이렇게 나누는 이유에 대해서 알아보셨냐는 것이었다. 사실 알아보지 않고 코드를 보면서 아, 이렇게 쓰는구나까지만 하고 🙄넘어갔던 부분이라 뜨끔함. 설명을 해주시는 부분이 필드를 선언하고, 속성으로써 구현을 하면서 크로스 스레딩을 막기 위해서 라고 하셨다. (코드리뷰를 할때는 메모를 하면서 듣기가 애매해서 최대한 기억하려고 애쓰면서 키워드를 잡아내는 식으로 기억)
field : Class 내에 데이터를 저장하기 위해서 사용
Property : field에 대한 접근을 제어하기 위해서 사용, 속성은 필드에 대한 읽기 및 쓰기 접근을 허용하거나 제한할 수 있다.
개념적으로만 내가 이해하고 있던 캡슐화에 대한 설명이었다.
field와 속성을 나눠서 구분하지 않으면 TextBox와 다를바가 없다고 설명을 해주셨는데 UI요소로 외부코드로 접근이 가능한 상황을 말씀하신것 같다.

Cross Threading : UI 스레드와 백그라운드 스레드 간의 데이터 접근 시 발생. UI 스레드가 아닌 다른 스레드에서 UI 요소를 직접 수정하는 경우 예외가 발생한다.
속성과 메서드의 차이는?
try //finally 문까지
{
if (messageQueue.TryDequeue(out var message))
{
Application.Current.Dispatcher.Invoke(() =>
{
Console.WriteLine($"{DateTime.Now}: {message} sent ");
});
switch (message)
{
case "RandomNumber":
SendRandomNumber();
break;
case "RandomMessage":
SendRandomMessage();
break;
case "MessageFromFile":
SendMessageFromFile();
break;
default:
SendMessageFromTextBox(message);
break;
}
}
else
{
Task.Delay(100).Wait(); // 메시지가 없을 때 잠시 대기
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in SendMessages: {ex.Message}");
}
//finally : 예외의 발생 여부와 상관없이 항상 실행
try, catch, finally : 예외처리에 사용되는 구문. try에서 예외가 발생하면 catch에서 예외를 처리하게된다. 내부에서 예외를 처리하는 방법으로 외부로 뱉어내지 않는다. finally부분은 예외의 여부와 상관없이 실행된다. 주로 자원 해제나 정리 작업을 수행하는 데 사용.
⭐
public class RelayCommand : ICommand //이거 돌아가는거 이해하기
{
private readonly Action execute;
private readonly Func<bool> canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter) => canExecute == null || canExecute();
public void Execute(object parameter) => execute();
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
BlockingCollection으로 Queue를 구성했던것을 ConcurrentQueue로 개선했다.
Queue<T> : 단일 스레드 환경에서 사용되며 멀티 스레드로 사용하려면 동기화가 필요하다. Enqueue, Dequeue

ConcurrentQueue<T> Enqueue, TryDequeue(출력시 out 지원)

BlockingCollection<T>
