다음을 통해 공유


MVVM 도구 키트를 사용하여 MVVM 구현

이제 프로젝트 구조가 준비되었으므로 MVVM 도구 키트를 사용하여 MVVM 패턴 구현을 시작할 수 있습니다. 이 단계에서는 속성 변경 알림 및 ObservableObject 명령 구현과 같은 RelayCommand MVVM 도구 키트의 기능을 활용하는 ViewModel을 만드는 작업이 포함됩니다.

MVVM 도구 키트 NuGet 패키지 설치

WinUINotes 및WinUINotes.Bus 프로젝트 모두에 MVVM 도구 키트를 설치해야 합니다.

Visual Studio 사용

  1. 솔루션 탐색기에서 WinUINotes.Bus 프로젝트를 마우스 오른쪽 단추로 클릭합니다.
  2. NuGet 패키지 관리를 선택합니다.
  3. CommunityToolkit.Mvvm을 검색하고 안정적인 최신 버전을 설치합니다.
  4. WinUINotes 프로젝트에 대해 이 단계를 반복합니다.

.NET CLI 사용

또는 .NET CLI를 사용하여 패키지를 설치할 수 있습니다.

dotnet add WinUINotes.Bus package CommunityToolkit.Mvvm
dotnet add WinUINotes package CommunityToolkit.Mvvm

모델 계층에 대한 디자인 결정

MVVM을 구현하는 경우 ViewModels와 관련하여 모델 클래스를 구성하는 방법을 결정하는 것이 중요합니다. 이 자습서에서는 모델 클래스(NoteAllNotes)가 데이터 표현, 비즈니스 논리 및 데이터 스토리지 업데이트를 담당합니다. ViewModels는 UI 상호 작용을 위해 관찰 가능한 속성, 변경 알림 및 명령을 처리합니다.

더 간단한 구현에서는 비즈니스 논리 또는 데이터 액세스 메서드 없이 모델 클래스에 일반 이전 POCO(CLR 개체)를 사용할 수 있습니다. 이 경우 ViewModels는 서비스 계층을 통해 모든 데이터 작업을 처리합니다. 그러나 이 자습서의 경우 모델 클래스에는 문제를 보다 명확하게 구분하고 ViewModels를 프레젠테이션 논리에 집중하기 위해 노트를 로드, 저장 및 삭제하는 메서드가 포함됩니다.

메모 모델 이동

클래스를 NoteWinUINotes.Bus 프로젝트로 이동합니다. 데이터 표현 및 상태 관리를 위한 몇 가지 논리가 있지만 MVVM 도구 키트 기능이 없는 간단한 모델 클래스로 유지됩니다. ViewModels는 모델 자체가 아니라 관찰 가능한 속성 및 변경 알림을 처리합니다.

  1. WinUINotes.Bus 프로젝트에서 Models라는 새 폴더를 만듭니다.

  2. Note.cs WinUINotes 프로젝트에서 WinUINotes.Bus/Models 폴더로 파일을 이동합니다.

  3. 새 위치와 일치하도록 네임스페이스를 업데이트합니다.

    namespace WinUINotes.Models
    {
        public class Note
        {
            // Existing code remains unchanged
            ...
        }
    }
    

클래스는 Note 간단한 데이터 모델입니다. ViewModels는 관찰 가능한 속성을 관리하고 변경 내용을 UI에 알리기 때문에 변경 알림이 필요하지 않습니다.

AllNotes 모델 이동

클래스를 AllNotesWinUINotes.Bus 프로젝트로 이동합니다.

  1. AllNotes.cs WinUINotes 프로젝트에서 WinUINotes.Bus/Models 폴더로 파일을 이동합니다.

  2. 새 위치와 일치하도록 네임스페이스를 업데이트합니다.

    namespace WinUINotes.Models
    {
        public class AllNotes
        {
            // Existing code remains unchanged
            ...
        }
    }
    

클래스 NoteAllNotes 마찬가지로 간단한 모델 클래스입니다. ViewModel은 관찰 가능한 동작을 처리하고 노트 컬렉션을 관리합니다.

AllNotesViewModel 만들기

  1. WinUINotes.Bus 프로젝트에서 ViewModels라는 새 폴더를 만듭니다.

  2. 다음 콘텐츠와 함께 명명된 AllNotesViewModel.cs 새 클래스 파일을 추가합니다.

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    using System.Threading.Tasks;
    using WinUINotes.Models;
    
    namespace WinUINotes.ViewModels
    {
        public partial class AllNotesViewModel : ObservableObject
        {
            private readonly AllNotes allNotes;
    
            [ObservableProperty]
            private ObservableCollection<Note> notes;
    
            public AllNotesViewModel()
            {
                allNotes = new AllNotes();
                notes = new ObservableCollection<Note>();
            }
    
            [RelayCommand]
            public async Task LoadAsync()
            {
                await allNotes.LoadNotes();
                Notes.Clear();
                foreach (var note in allNotes.Notes)
                {
                    Notes.Add(note);
                }
            }
        }
    }
    

AllNotesViewModel UI에 표시되는 노트 컬렉션을 관리합니다.

  • [ObservableProperty]: 필드가 notes 변경 알림이 포함된 공용 Notes 속성을 자동으로 생성합니다. 컬렉션이 Notes 변경되면 UI가 자동으로 업데이트됩니다.
  • allNotes 모델: 이 프라이빗 필드는 실제 데이터 작업을 처리하는 모델의 인스턴스 AllNotes 를 보유합니다.
  • [RelayCommand]: 이 특성은 LoadCommand 메서드에서 LoadAsync() 속성을 생성하여 UI가 데이터 바인딩을 통해 로드 작업을 트리거할 수 있도록 합니다.
  • LoadAsync() 메서드: 이 메서드는 모델에서 노트를 로드하고, 현재 관찰 가능한 컬렉션을 지우고, 로드된 노트로 채웁니다. 이 패턴은 UI 바인딩된 컬렉션이 기본 데이터와 동기화된 상태로 유지되도록 합니다.

모델(데이터 작업)과 allNotes 관찰 가능한 컬렉션(UI 바인딩) 간의 Notes 분리는 문제를 분리하고 View가 ViewModel의 데이터와 동기화된 상태로 유지되는 주요 MVVM 패턴입니다.

다음 문서에서 자세히 알아보세요.

NoteViewModel 만들기

  1. ViewModels 폴더에서 다음과 같은 NoteViewModel.cs새 클래스 파일을 추가합니다.

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System;
    using System.Threading.Tasks;
    using WinUINotes.Models;
    
    namespace WinUINotes.ViewModels
    {
        public partial class NoteViewModel : ObservableObject
        {
            private Note note;
    
            [ObservableProperty]
            [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
            [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
            private string filename = string.Empty;
    
            [ObservableProperty]
            [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
            private string text = string.Empty;
    
            [ObservableProperty]
            private DateTime date = DateTime.Now;
    
            public NoteViewModel()
            {
                this.note = new Note();
                this.Filename = note.Filename;
            }
    
            public void InitializeForExistingNote(Note note)
            {
                this.note = note;
                this.Filename = note.Filename;
                this.Text = note.Text;
                this.Date = note.Date;
            }
    
            [RelayCommand(CanExecute = nameof(CanSave))]
            private async Task Save()
            {
                note.Filename = this.Filename;
                note.Text = this.Text;
                note.Date = this.Date;
                await note.SaveAsync();
    
                // Check if the DeleteCommand can now execute
                // (it can if the file now exists)
                DeleteCommand.NotifyCanExecuteChanged();
            }
    
            private bool CanSave()
            {
                return note is not null
                    && !string.IsNullOrWhiteSpace(this.Text)
                    && !string.IsNullOrWhiteSpace(this.Filename);
            }
    
            [RelayCommand(CanExecute = nameof(CanDelete))]
            private async Task Delete()
            {
                await note.DeleteAsync();
                note = new Note();
            }
    
            private bool CanDelete()
            {
                // Note: This is to illustrate how commands can be
                // enabled or disabled.
                // In a real application, you shouldn't perform
                // file operations in your CanExecute logic.
                return note is not null
                    && !string.IsNullOrWhiteSpace(this.Filename)
                    && this.note.NoteFileExists();
            }
        }
    }
    

다음은 NoteViewModel 몇 가지 주요 MVVM 도구 키트 기능을 보여 줍니다.

  • [ObservableProperty] filename: , textdate 필드는 변경 알림 지원을 사용하여 공용 속성(Filename, Text,Date)을 자동으로 생성합니다.
  • [NotifyCanExecuteChangedFor]: 이 특성은 Filename 또는 Text가 변경될 때 연결된 명령이 실행 가능한지 여부를 다시 평가하도록 보장합니다. 예를 들어 텍스트를 입력할 때 저장 단추는 유효성 검사 논리에 따라 자동으로 사용하거나 사용하지 않도록 설정합니다.
  • [RelayCommand(CanExecute = nameof(CanSave))]: 이 특성은 SaveCommand 유효성 검사 메서드 CanSave()에 바인딩된 속성을 생성합니다. TextFilename 모두 값이 있을 때에만 이 명령이 활성화됩니다.
  • InitializeForExistingNote(): 이 메서드는 기존 노트의 데이터를 ViewModel 속성에 로드한 다음 데이터 바인딩을 통해 UI를 업데이트합니다.
  • 저장 논리: 메서드는 Save() 기본 Note 모델을 현재 속성 값으로 업데이트하고 모델에 대한 호출을 수행 SaveAsync() 합니다. 저장한 후에는 다시 평가해야 한다는 알림 DeleteCommand 이 표시됩니다(파일이 현재 존재하고 삭제할 수 있기 때문에).
  • 논리 삭제: 메서드는 Delete() 메모 모델을 호출 DeleteAsync() 하고 빈 메모를 새로 만듭니다.

이 자습서의 뒷부분에서 파일 서비스를 통합하여 실제 파일 작업을 처리하고 MVVM Toolkit의 WeakReferenceMessenger 클래스를 사용하여 느슨하게 결합된 상태로 메모가 삭제될 때 앱의 다른 부분에 알립니다.

다음 문서에서 자세히 알아보세요.

ViewModels를 사용하도록 뷰 업데이트

이제 XAML 페이지를 업데이트하여 새 ViewModels에 바인딩해야 합니다.

AllNotesPage 보기 업데이트

  1. AllNotesPage.xaml에서 ItemsView의 바인딩을 ViewModel의 Notes 속성을 사용하도록 ItemsSource 업데이트합니다.

    <ItemsView ItemsSource="{x:Bind viewModel.Notes}"
    ...
    
  2. AllNotesPage.xaml.cs 다음과 같이 파일을 업데이트합니다.

    using Microsoft.UI.Xaml;
    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using WinUINotes.ViewModels;
    
    namespace WinUINotes.Views
    {
        public sealed partial class AllNotesPage : Page
        {
            private AllNotesViewModel? viewModel;
    
            public AllNotesPage()
            {
                this.InitializeComponent();
                viewModel = new AllNotesViewModel();
            }
    
            private void NewNoteButton_Click(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(NotePage));
            }
    
            private void ItemsView_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
            {
                Frame.Navigate(typeof(NotePage), args.InvokedItem);
            }
    
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
    
                if (viewModel is not null)
                {
                    await viewModel.LoadAsync();
                }
            }
        }
    }
    

이 코드 비하인드 파일에서는 생성자가 AllNotesViewModel를 직접 인스턴스화합니다. 이 메서드는 페이지가 OnNavigatedTo()로 탐색될 때 ViewModel에서 LoadAsync() 메서드를 호출합니다. 이 메서드는 스토리지에서 노트를 로드하고 관찰 가능한 컬렉션을 업데이트합니다. 이 패턴은 사용자가 모든 노트 페이지로 이동할 때 데이터가 항상 새로 고쳐지도록 합니다.

이 자습서의 뒷부분에서는 종속성 주입을 사용하도록 이 코드를 리팩터링하여 ViewModel을 직접 만드는 대신 페이지 생성자에 삽입할 수 있습니다. 이 방법을 사용하면 테스트 용이성이 향상되고 ViewModel 수명 주기를 더 쉽게 관리할 수 있습니다.

NotePage 보기 업데이트

  1. NotePage.xaml에서 TextBoxTextHeader 바인딩을 ViewModel의 속성을 사용하도록 업데이트하십시오. StackPanel 버튼이 Click 이벤트 대신 명령에 바인딩되도록 업데이트합니다.

    ...
    <TextBox x:Name="NoteEditor"
             Text="{x:Bind noteVm.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             AcceptsReturn="True"
             TextWrapping="Wrap"
             PlaceholderText="Enter your note"
             Header="{x:Bind noteVm.Date.ToString()}"
             ScrollViewer.VerticalScrollBarVisibility="Auto"
             MaxWidth="400"
             Grid.Column="1"/>
    
    <StackPanel Orientation="Horizontal"
                HorizontalAlignment="Right"
                Spacing="4"
                Grid.Row="1" Grid.Column="1">
        <Button Content="Save" Command="{x:Bind noteVm.SaveCommand}"/>
        <Button Content="Delete" Command="{x:Bind noteVm.DeleteCommand}"/>
    </StackPanel>
    ...
    

    또한 변경 내용이 사용자가 입력하는 대로 ViewModel에 전송되도록 UpdateSourceTriggerTextBox.Text 바인딩에 설정합니다. 이 설정을 사용하면 Save 단추를 입력에 따라 실시간으로 사용하거나 사용하지 않도록 설정할 수 있습니다.

  2. NotePage.xaml.cs에서 NoteViewModel을(를) 사용하도록 코드를 업데이트합니다.

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using WinUINotes.Models;
    using WinUINotes.ViewModels;
    
    namespace WinUINotes.Views
    {
        public sealed partial class NotePage : Page
        {
            private NoteViewModel? noteVm;
    
            public NotePage()
            {
                this.InitializeComponent();
            }
    
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
                noteVm = new NoteViewModel();
    
                if (e.Parameter is Note note && noteVm is not null)
                {
                    noteVm.InitializeForExistingNote(note);
                }
            }
        }
    }
    

    이제 단추가 직접 명령에 바인딩 되므로 ClickSaveDelete 이벤트가 제거됩니다. 메서드 NoteViewModel 에서 OnNavigatedTo() 인스턴스화됩니다. 매개 변수가 Note 전달되면 기존 메모 데이터를 사용하여 ViewModel을 초기화합니다.

다음 문서에서 자세히 알아보세요.