์ฃผ๋ง ์•„์นจ #3

C# ๋ฐ MVVM ํˆดํ‚ท์œผ๋กœ Blazor ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋งŒ๋“ค๊ธฐ | bromix

MVVM ํˆดํ‚ท ๋ฐ RelayCommands๋กœ Blazor ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋งŒ๋“ค๊ธฐ | bromix

MVVM ํˆดํ‚ท์„ ์‚ฌ์šฉํ•œ ๋ธ”๋ ˆ์ด์ €์—์„œ ์‰ฝ๊ฒŒ ๋น„๋™๊ธฐ ๋ช…๋ น ์ทจ์†Œํ•˜๊ธฐ | bromix

์ด์™ธ์— ์ฝ์„๋งŒํ•œ ๊ธ€๋“ค์ž…๋‹ˆ๋‹ค.

C# ํŒ: ๋ณด๊ฐ„ ๋ฌธ์ž์—ด ์„œ์‹ ์ง€์ • | Code4IT

.NET์—์„œ exe์˜ Windows ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๋งค๋‹ˆํŽ˜์ŠคํŠธ ์ฝ๊ธฐ | meziantou

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋กœ๊ทธ ์ˆ˜์ค€ ์ œ์–ด | Steven Giesel

.NET 7๋กœ ์„ฑ์žฅํ•˜๋Š” ์ตœ์†Œ API | Christian Nagel

6๊ฐœ์˜ ์ข‹์•„์š”

์—”ํ‹ฐํ‹ฐ ํ”„๋ ˆ์ž„์›Œํฌ ์ฝ”์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—”ํ‹ฐํ‹ฐ ์ €์žฅ ํ›„ ์ฆ‰์‹œ MediatR ์•Œ๋ฆผ ๋ณด๋‚ด๊ธฐ | no dogma blog

๋ช‡ ์ฃผ ์ „์— ์—”ํ‹ฐํ‹ฐ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๊ฐœ์ฒด๋ฅผ ์ €์žฅํ•œ ์งํ›„์— ์•ก์„ธ์Šคํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ฒŒ์‹œ๋ฌผ์„ ์ž‘์„ฑํ–ˆ๋Š”๋ฐ, ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์—์„œ๋Š” ์ €์žฅ๋œ ๊ฐ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด ์ฝ˜์†”์— ํ•œ ์ค„์„ ์ถœ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.

์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ๋˜์—ˆ๋‹ค๋Š” ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋ ค๊ณ  ํ•  ๋•Œ ์ด ๋ฐฉ๋ฒ•์ด ์–ด๋–ป๊ฒŒ ์œ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋ณด์—ฌ๋“œ๋ฆฌ๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ €์žฅ๋œ ์งํ›„ MediatR ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์—์„œ๋Š” MediatR ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ฉฐ, ์ด์— ๋Œ€ํ•œ ์ข‹์€ ๋ฌธ์„œ๊ฐ€ ์žˆ์œผ๋ฉฐ, ์ €๋„ ์ด์— ๋Œ€ํ•œ ๋ช‡ ๊ฐœ์˜ ๊ฒŒ์‹œ๋ฌผ์„ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•ธ๋“ค๋Ÿฌ๋Š” ProductNotificationHandler.cs ํŒŒ์ผ์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋ฉฐ, ์ฝ˜์†”์— ํ•œ ์ค„์„ ์ธ์‡„ํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ €์žฅ๋œ ์งํ›„ ์•Œ๋ฆผ์„ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด DbContext์˜ SaveChangesAsync ๋ฉ”์„œ๋“œ๋ฅผ ์žฌ> ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

5๊ฐœ์˜ ์ข‹์•„์š”

๋กœ์ง ์ฝ”๋“œ๋ฅผ ๋ทฐ๋ชจ๋ธ๋กœ ์˜ฎ๊ธฐ๋Š” ์•„์ด๋””์–ด๋ฅผ ์œ„ํ•ด ๊ตณ์ด MVVM ํˆดํ‚ท์„ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์žˆ๋Š”์ง€๋Š” ์˜๋ฌธ์ž…๋‹ˆ๋‹ค.

MVVM ํˆดํ‚ท์œผ๋กœ ์ธํ•ด ๋ทฐ๋ชจ๋ธ์— ์ด ์„ธ๊ฐœ์˜ Notify ์ฝ”๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€๋งŒ, ๋ทฐ๊ฐ€ ๊ตฌ๋…ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฒคํŠธ๋Š” ํŠธ๋ฆฌ๊ฑฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

// PropertyChanged = null
    public async Task LoadDataAsync()
    {
        IsLoading = true; // Notify

        try
        {
            // ...
            Forecasts = await _weatherService.GetForecastAsync(DateTime.Now); // Notify
        }
        finally
        {
            IsLoading = false; // Notify
        }
    }

๊ฐ•์ขŒ์˜ ์ฝ”๋“œ๋Š” MVVM ํˆดํ‚ท์ด ์—†์–ด๋„ ๋ทฐ๊ฐ€ ๋ทฐ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

internal sealed class WeatherViewModel
{
    private readonly WeatherForecastService _weatherService;

    public WeatherViewModel(WeatherForecastService weatherService)
    {
        _weatherService = weatherService;
    }

   public bool IsLoading;

   public WeatherForecast[] Forecasts = Array.Empty<WeatherForecast>();

    public async Task LoadDataAsync()
    {
        IsLoading = true;

        try
        {
            await Task.Delay(TimeSpan.FromSeconds(2)); // simulate loading
            Forecasts = await _weatherService.GetForecastAsync(DateTime.Now);
        }
        finally
        {
            IsLoading = false;
        }
    }
}

MVVM ํˆดํ‚ท์ด ๋ธ”๋ ˆ์ด์ €์—์„œ ํž˜์„ ๋ฐœํœ˜ํ•˜๋ ค๋ฉด, ๋ธ”๋ ˆ์ด์ €๊ฐ€ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ง€์›ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์€๋ฐ,

//.razor ์—์„œ
@BindingContext WeatherViewModel
//ํ˜น์€
@DataContext WeatherViewModel

(์–‘๋ฐฉํ–ฅ) ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์€ Notify => Read/Notify => Read ์˜ ๋ผ์šด๋“œ ํŠธ๋ฆฝ ๊ตฌ์กฐ์ด๊ธฐ์—, ๋ธ”๋ ˆ์ด์ € ์„œ๋ฒ„ ์•ฑ์—์„œ๋Š” SignalR ๋ฐ์ดํ„ฐ๋Ÿ‰์ด ํญ์ฆํ•˜๋Š” ๋ถ€์ž‘์šฉ์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก , ๋ธ”๋ ˆ์ด์ € ์›น์–ด์…ˆ๋ธ”๋ฆฌ ์•ฑ์—์„œ๋Š” ์ฑ„ํƒํ•  ๋งŒํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

5๊ฐœ์˜ ์ข‹์•„์š”

๋ฌธ์ž์—ด ๋ณด๊ฐ„์€ ์ฆ๊ฒจ์“ฐ๋Š” ๊ธฐ๋Šฅ์ด์—ˆ๋Š”๋ฐ ์„œ์‹๋„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค๋‹ˆ ์—„์ฒญ ์‹ ๊ธฐํ•˜๋„ค์š” ใ…Žใ…Ž

2๊ฐœ์˜ ์ข‹์•„์š”

์˜๊ฒฌ ์ง€์  ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

์ „๊ฐœํ•œ ๋‚ด์šฉ (ํŠนํžˆ ์ฒซ ๋ฒˆ์งธ ๋‚ด์šฉ)์ด ์นœ์ ˆํ•˜์ง€ ์•Š์•„์„œ ์ƒ๊ธด ๋ฌธ์ œ๋กœ ๋ณด์ด๋ฉฐ ์‚ฌ์‹ค ์—ฐ์†๋œ ์‹œ๋ฆฌ์ฆˆ์˜ ๋งˆ๋ฌด๋ฆฌ โ€“ MVVM ํˆดํ‚ท์œผ๋กœ ๋ช…๋ น์–ด ์‚ฌ์šฉ ๋ฐ ๋น„๋™๊ธฐ ์ทจ์†Œ๊ฐ€ ์ €์ž๊ฐ€ ์ „๋‹ฌํ•˜๊ณ ์ž ํ•˜๋Š” ์ฃผ๋œ ๋‚ด์šฉ์œผ๋กœ ์ดํ•ด๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ ๋งŒ์œผ๋กœ๋„ Blazor์—์„œ MVVM ํˆดํ‚ท์„ ์‚ฌ์šฉํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์ข€ ๋” ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋งŒ๋“ค์–ด ์ค€๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

...
<button disabled="@ViewModel.CountCommand.IsRunning" @onclick="() => ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>
...
    [RelayCommand]
    public async Task OnCount()
    {
        for (var i = 0; i < 200; i++)
        {
            Count++;

            await Task.Delay(10);
        }
    }

ํ•œํŽธ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์˜ ํ‘œ์‹œ๊ฐ€ ์•„๋‹Œ ์ด์ƒ Blazor์—์„œ๋Š” ์ž…๋ ฅ(๋ฒ„ํŠผ์˜ ํด๋ฆญ ๋“ฑ)์˜ ์ด๋ฒคํŠธ์— ์˜ํ•ด ํ™”๋ฉด ๊ฐฑ์‹ ์ด ์ด๋ฃจ์–ด์ง€๋ฏ€๋กœ INotifyPropertyChanged ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜์ง€ ์•Š์•„๋„ ๋Œ€๋ถ€๋ถ„์˜ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ฐ€๋ น,

์—์„œ await ์ด์ „์— IsLoading์˜ ๋ณ€๊ฒฝ์ด ํ™”๋ฉด์— (์ž๋™์œผ๋กœ) ์ ์šฉ๋˜๋ฉฐ LoadDataAsync() ๋ฉ”์†Œ๋“œ์˜ ์ฝœ์ด ๋๋‚˜๋Š” ์‹œ์ ์— ๋‹ค์‹œ IsLoading์˜ ๋ณ€๊ฒฝ๋œ ์ƒํƒœ๊ฐ€ ํ™”๋ฉด์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ 1๋ถ€ํ„ฐ 100๊นŒ์ง€ 10ms ๋‹จ์œ„๋กœ ์ถœ๋ ฅํ•˜๋Š” ๋“ฑ์€ ๋™์ž‘ํ•˜์ง€ ์•Š์ฃ . ์ด๋ถ€๋ถ„์„ ๋‹ค์Œ์˜ ๋ฐฉ์‹์œผ๋กœ ๊ฐ„๋‹จํžˆ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์ฒ˜๋Ÿผ ViewModel์„ MVVM ํˆดํ‚ท์œผ๋กœ ๊ตฌ์„ฑํ•œ ํ›„

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

using System.ComponentModel;

namespace BlazorApp41.ViewModels;

public partial class IndexViewModel : ObservableObject
{
    [ObservableProperty]
    private int _count;

    public Action StateHasChanged { get; set; } = () => { };


    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);

        StateHasChanged();
    }

    [RelayCommand]
    public async Task OnCount()
    {
        for (var i = 0; i < 200; i++)
        {
            Count++;

            await Task.Delay(10);
        }
    }
}

StateHasChanged Action ๋”œ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ๋‹ค์Œ์ฒ˜๋Ÿผ ํ•˜๋ฉด

@code {
    protected override void OnInitialized()
    {
        ViewModel.StateHasChanged = () => InvokeAsync(StateHasChanged);
    }
}

๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๋™์ž‘์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋ฒ„ํŠผ์€ ๋น„ํ™œ์„ฑํ™” ๋˜๋ฉฐ ์ˆซ์ž๊ฐ€ 10ms ๊ฐ„๊ฒฉ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@page "/"
@using BlazorApp41.ViewModels;
@inject IndexViewModel ViewModel

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

<h2>@ViewModel.Count</h2>

<button disabled="@ViewModel.CountCommand.IsRunning" @onclick="async () => await ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

@code {
    protected override void OnInitialized()
    {
        ViewModel.StateHasChanged = () => InvokeAsync(StateHasChanged);
    }
}

Blazor Webassembly์—์„œ๋Š” ๋ง์”€ํ•˜์‹  ๊ฒƒ ์ฒ˜๋Ÿผ ViewModel์˜ ์†์„ฑ์ด ๋ณ€๊ฒฝ๋จ์— ๋”ฐ๋ผ ๊ฐฑ์‹ ๋˜๋Š” ํ™”๋ฉด์˜ ๋ Œ๋”๋ง์„ ์›น๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•˜๋ฏ€๋กœ ๋ฌธ์ œ ๋  ๊ฒƒ์ด ์ „ํ˜€ ์—†์œผ๋‚˜ Blazor Server ๊ฒฝ์šฐ html ๋ Œ๋”๋ง์„ ์„œ๋ฒ„์—์„œ ํ•˜๋ฉฐ ViewModel ์ธ์Šคํ„ด์Šค ์—ญ์‹œ ์„œ๋ฒ„์— ์žˆ์–ด์„œ ViewModel์˜ ์†์„ฑ ๋ณ€๊ฒฝ์— ์˜ํ•œ ํ™”๋ฉด ๋ Œ๋”๋ง์— ์˜ํ•œ ๋ฐ์ดํ„ฐ๋Ÿ‰์˜ ์ฆ๊ฐ€๋Š” ๋‹น์—ฐํžˆ ๋ฐœ์ƒํ•˜์ง€๋งŒ ์šฐ๋ คํ•˜์‹œ๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์˜ ํ‘œํ˜„์ด ์•„๋‹Œ ์ด์ƒ Blazor Server์˜ ์‚ฌ์šฉ๋ชฉ์ ์— ๋ฒ—์–ด๋‚˜์ง€๋Š” ์•Š๋Š”๋‹ค๋ผ๋Š” ๊ฒƒ์ด ์ €์˜ ์˜๊ฒฌ์ž…๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์€ MVVM ํˆดํ‚ท์„ Blazor์—์„œ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ์—†์œผ๋ฉฐ ๋‹ค๋ฅธ ํ™˜๊ฒฝ๊ณผ ๋™์ผํ•œ ๋ฐ”์ธ๋”ฉ ํ˜œํƒ์„ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

4๊ฐœ์˜ ์ข‹์•„์š”

๊ด€๋ จํ•ด์„œ ์ด๋ ‡๊ฒŒ ์“ฐ๋ฉด ๊ดœ์ฐฎ์ง€ ์•Š์„๊นŒ ์ƒ˜ํ”Œ์„ ๋งŒ๋“ค์–ด๋ดค์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๊ณ ์š”,
ViewModel โ†’ ViewModelBase โ†’ IStateHasChanged
ViewModelComponent โ†’ OwningComponentBase

IoC๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด Program.cs์— IndexViewModel๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๊ณ 

builder.Services.AddScoped<IndexViewModel>();

์ดํ›„ ์‚ฌ์šฉ๋ฒ•์€ ๊ฑฐ์˜ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

| IndexViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

using N31.BlazorServerMVVM.Base;

namespace N31.BlazorServerMVVM.ViewModels;

public partial class IndexViewModel : ViewModelBase
{
    [ObservableProperty] private int _count;
    [ObservableProperty] private int _inputCount = 200;

    [RelayCommand]
    public async Task OnCountAsync(CancellationToken ct)
    {
        try
        {
            for (var i = 0; i < InputCount; i++)
            {
                Count++;

                await Task.Delay(10, ct);
            }
        }
        catch (OperationCanceledException)
        {
        }
    }
}

| Index.razor

@inherits ViewModelComponent<IndexViewModel>

@page "/"

@using N31.BlazorServerMVVM.Base;
@using N31.BlazorServerMVVM.ViewModels;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<h2>@ViewModel.Count</h2>

<div>
    <button disabled="@ViewModel.CountCommand.IsRunning" @onclick="() => ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>
    <button disabled="@(!ViewModel.CountCommand.CanBeCanceled)" @onclick="@ViewModel.CountCommand.Cancel">Cancel</button>
</div>

<div>
    <input disabled="@ViewModel.CountCommand.IsRunning" @bind="@ViewModel.InputCount" @bind:event="oninput" />
    <input disabled="@ViewModel.CountCommand.IsRunning" @bind="@ViewModel.InputCount" @bind:event="oninput" />
</div>

<SurveyPrompt Title="How is Blazor working for you?" />

Blazor๋Š” razor์˜ ์ƒ์† ํ‘œํ˜„์— ์ œ๋„ค๋ฆญ์„ ์“ธ ์ˆ˜ ์žˆ์–ด์„œ ViewModel์„ ์ข€ ๋” ์‚ฌ์šฉํ•˜๊ธฐ ํŽธํ•œ ๋Š๋‚Œ์ž…๋‹ˆ๋‹ค.

@inherits ViewModelComponent<IndexViewModel>
5๊ฐœ์˜ ์ข‹์•„์š”

์ €์˜ ์š”์ ์ด "๊ตฌ๋…ํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š”๋ฐ, ๊ตฌ๋…์„ ์ „์ œ๋กœ ํ•œ ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ• ๊นŒ?"์ž…๋‹ˆ๋‹ค.

MVVM ํˆดํ‚ท์€ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ „์ œ๋กœ, ๋ฐ”์ธ๋”ฉ ์†Œ์Šค(๋ทฐ๋ชจ๋ธ)๋ฅผ ์œ„ํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด ๋„๊ตฌ๋Š” ๋ฐ”์ธ๋”ฉ ํƒ€๊ฒŸ(๋ทฐ)์ด ์ ์ ˆํ•œ ์žฅ์น˜๋“ค( BindableProperty, Command, RoutedEvent, Binding ๋“ฑ)์„ ๊ฐ–์ถ”๊ณ  ์žˆ์„ ๋•Œ ํŽธ๋ฆฌํ•œ ๊ฒƒ์ด์ง€, ์ด ์žฅ์น˜๋“ค์„ ์‚ฌ์šฉ์ž ์ผ์ผ์ด ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๊ธ์–ด ๋ถ€์Šค๋Ÿผ์— ์ง€๋‚˜์ง€ ์•Š๊ฒ ์ฃ .

๋ฆด๋ ˆ์ด ์ปค๋งจ๋“œ ์˜ˆ์ œ ์ฝ”๋“œ๊ฐ€ ์ด๋Ÿฌํ•œ ์ ์„ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ธ”๋ ˆ์ด์ € ์š”์†Œ๊ฐ€ Command ์†์„ฑ์„ ์ œ๊ณตํ–ˆ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ–ˆ์„ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

<button @OnClickCommand = "BindingContext.CountCommand" disabled="BindingContext.CountCommand.IsRunning" ...

๋˜ํ•œ, BindableProperty๋„ ์ œ๊ณตํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, StateHasChanged ๋ฉ”์„œ๋“œ๋กœ ์ด๋ฅผ ํ‰๋‚ด๋‚ธ ๊ฒƒ์ด์ฃ .

์ด๋Ÿฌํ•œ ํ‰๋‚ด๊ฐ€ ์•ฝ๊ฐ„์˜ ๋ถ€ํ•˜๋ฅผ ๋Š˜๋ ค๋„, ๊ธฐ๋Šฅ ์ƒ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค๋ฉด, WPF/UWP/MAUI/Blazor WASM ์ฒ˜๋Ÿผ ์‹ฑ๊ธ€ ์ธ์Šคํ„ด์Šค ๊ธฐ๋ฐ˜ ์•ฑ์—์„œ๋Š” ๊ตณ์ด ๋ถˆํ•„์š”ํ•˜๋‹ค๊ณ  ๋งํ•  ๊ฒƒ๊นŒ์ง€๋Š” ์—†์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, Blazor Server์•ฑ์€ ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ์ˆ˜๋ฐฑ์—์„œ ์ˆ˜์ฒœ๊ฐœ๊นŒ์ง€ ์กด์žฌํ•˜๋Š” ๊ฒƒ์„ ์ „์ œํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์•ฝ๊ฐ„์˜ ๋ถ€ํ•˜ ์ฆ๊ฐ€๋„ ์ ‘์†์ž ์ˆ˜์— ์ •๋น„๋ก€ํ•˜๊ฒŒ ์ฆ๊ฐ€ํ•˜๊ฒŒ ๋˜์–ด, ์‹ ์ค‘ํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด ๊ธ€๊ณผ ๊ด€๋ จํ•œ ๋…ผ์˜๋ฅผ ์ด์–ด ๊ฐ€๋ฉด์„œ ๋ฌธ๋“

๋ธ”๋ ˆ์ด์ €์˜ ์ด๋ฒคํŠธ ์ „ํŒŒ ์ˆ˜๋‹จ์œผ๋กœ delegate ๊ฐ€ ์•„๋‹Œ EventCallback ์„ ์ฑ„ํƒํ•œ ๊ฒƒ์€ ๋ถ€ํ•˜ ๋ถ€๋‹ด์ด ๊ฐ€์žฅ ํฐ ์›์ธ์ด์ง€ ์•Š์„๊นŒ?

ํ•˜๋Š” ์ƒ๊ฐ๋„ ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ฐธ์กฐ ๊ฐ์ฒด์ธ delegate๋Š” ์ƒํƒœ๋ฅผ ์ €์žฅํ•ด์•ผ ํ•˜๋Š” ๋ฐ˜๋ฉด, ๊ฐ’ ๊ฐ์ฒด์ธ EventCallback์€ ๊ทธ๋ ‡์ง€ ์•Š์ฃ .

๊ทธ๋Ÿผ์—๋„, ์› ๊ธ€์˜ ์˜๋„ ์ค‘, ๋ทฐ๋ชจ๋ธ์— ๋ทฐ์˜ ์ƒํƒœ๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ํŒจํ„ด์€ ๋ธ”๋ ˆ์ด์ €์˜ ์ƒํƒœ ๊ด€๋ฆฌ ์ธก๋ฉด์—์„œ๋Š” ํ›Œ๋ฅญํ•œ ์„ ํƒ์ง€์ž„์—๋Š” ํ‹€๋ฆผ์—†์–ด ๋ณด์ž…๋‹ˆ๋‹ค.

์ถ”์‹ : ๊ทธ๋‚˜์ €๋‚˜ ์ด๋†ˆ์˜ ํ•ซ ๋ฆฌ๋กœ๋“œ ๋ฌธ์ œ๋Š” ์•„์ง๋„ ํ•ด๊ฒฐ์ด ์•ˆ๋์Šต๋‹ˆ๋‹ค. ๋น„์ฃผ์–ผ ์ŠคํŠœ๋””์˜ค ๋‹ค์‹œ ๊น”์•˜๋Š”๋ฐ๋„์š”. ใ… ใ… 

2๊ฐœ์˜ ์ข‹์•„์š”

VS 17.7 P1๋ถ€ํ„ฐ Blazor์—์„œ ๋‹ค์‹œ ํ•ซ๋ฆฌ๋กœ๋“œ๊ฐ€ ์ž˜ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

2๊ฐœ์˜ ์ข‹์•„์š”

์ €๋„ ์˜ค๋Š˜ ์ƒˆ๋ฒฝ์— ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ^^

๊ทผ๋ฐ, 17.6 ์—์„œ๋„ ์–ด๋–จ ๋•Œ๋Š” ๋˜๋‹ค๊ฐ€, ๋‹ค์‹œ ์•ˆ๋˜๋Š” ๊ฒฝํ—˜์„ ํ–ˆ๊ธฐ์— ์กฐ๊ธˆ ์ง€์ผœ๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ , ์ด๋ฒˆ ๋ฒ„์ „์€ ๋˜๊ธฐ๋Š” ํ•˜๋Š”๋ฐ, ๋ญ”๊ฐ€ ์ข€ ๋Š๋ฆฌ๋„ค์š”. ๊ธธ๋ฉด 5์ดˆ ์ •๋„ ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

2๊ฐœ์˜ ์ข‹์•„์š”