티스토리 뷰

반응형

XAML - Custom XAML Controls | Microsoft Learn

 

XAML - Custom XAML Controls

Table of contents Article 05/01/2019 20 minutes to read In this article --> May 2019 Volume 34 Number 5 [XAML] Custom XAML Controls By Jerry Nixon As an enterprise developer, you know your way around SQL Server. You know .NET Web services. And, for you, de

learn.microsoft.com

2019 MSDN 매거진에 등록된 Jerry Nixon의 글을 참고 했습니다.

Custom Controls

XAML에서 사용자 정의 컨트롤을 만드는 방법은 두가지가 있습니다. 

  • UserControl
    • 재사용 가능한 레이아웃을 만들기 위한 디자인 친화적인 접근 방법
  • Templated Control(CustomControl)
    • 개발자를 위한 맞춤형 API와 같이 유연한 레이아웃을 제공
    • 위의 문서에서는 Templated Control로 표현하고 있는데, 저는 CustomControl이라고 표현 하겠습니다.

어떤 방법으로 컨트롤을 만들지에 대해 도움이 되는 몇 가지 고려 사항을 알아보겠습니다.

  • Simplicity : 쉬운 것이 항상 간단한 것은 아니지만, 단순한 것은 항상 쉽습니다. 그래서, UserControl을 이용하면, 간단하고 쉽게 원하는 컨트롤을 만들 수 있습니다. 일반적인 XAML 개발자는 추가적으로 공부를 하지 않아도 만들 수 있는 레벨입니다. CustomControl은 컨트롤을 만들기 위한 정보를 추가적으로 공부해야 합니다.
  • Design experience : CustomControl의 디자인 레이아웃은 컨트롤을 만드는 중에는 확인 할 수 없습니다. 하지만, UserControl은 컨트롤을 만드는 중에도 확인이 가능합니다.
  • API surface : UserControl에 사용자 정의 프로퍼티, 이벤트, 메서드 등을 추가해서 작성할 수 있습니다. 그러나, 기본적으로 UserControl이라는 테두리를 벗어날 수는 없습니다. 하지만, CustomControl은 기본 베이스가 Control이기 때문에 더 다양한 기능을 구현할 수 있습니다.
  • Flexible visuals : UserControl로 만든 경우에는 화면 레이아웃을 개발자가 재정의 할 수 없습니다. 하지만, CustomControl은 재정의해서 사용할 수 있습니다.
  • ViewModel support : UserControl은 MVVM 패턴의 ViewModel과 1:1로 연결된 상태로 개발해서 사용할 수도 있습니다. 하지만, CustomControl은 UI와 Code가 분리되어 있는 형태로 존재하기 때문에 ViewModel을 연결시켜 사용하기가 어렵고, 그렇게 사용하지 않습니다.

MVVM 패턴을 사용하는 WPF 개발 프로젝트에서 대부분의 화면은 UserControl을 이용해서 개발하며, ViewModel을 함께 만들어 줍니다.

그러나, ViewModel을 포함하지 않고, UserControl를 이용해서 간단한 컨트롤을 개발하기도 하며, 특별한 모양이나 기능이 필요한 경우에는 CustomControl을 이용해서 컨트롤을 개발합니다.

화면에 배치해서 사용하는 용도의 컨트롤을 만드는 경우에는 UserControl이라고 하더라도, ViewModel을 만들어서 연결하지 않습니다. 그 이유는 컨트롤이 자체 ViewModel을 가지면, 화면의 ViewModel을 사용할 수 없기 때문입니다.

새로운 컨트롤 개발(팝업으로 출력하기 때문에 ViewModel 사용)

아래 이미와 같은 간단한 사용자 동의를 얻는 컨트롤을 만들려고 합니다.

  • 동의를 받아야하는 문장은 길기 때문에 스크롤이 가능해야합니다.
  • 체크를 해야지만, 제출을 클릭할 수 있습니다.
  • 제출 버튼을 누르면 다음 화면으로 이동하고, 종료 버튼을 누르면 애플리케이션을 종료합니다.
  • 팝업 형태로 출력 됩니다.

Wpf 프로젝트를 추가하고, MVVM nuget package와 Microsoft.Extensions.DependencyInjection를 설치합니다.

CommunityToolkit.Mvvm v8.0.0 버전을 사용하시면 됩니다. 기존 Microsoft.Toolkit.Mvvm과 동일한 nuget입니다.

App.xaml.cs에 들어가는 코드는 생략했습니다.

UserControl을 이용해서 개발

UserControl은 일반적으로 다른 컨트롤의 Child로 사용됩니다. 또한, Visual State Management, Resources 및 XAML 프레임워크의 필수 요소를 지원하는 모든 기능을 갖추고 있습니다. 개발자는 간단한 XAML 코드를 추가해서 화면을 구성할 수 있으며, 디자이너 친화적입니다.

 

UserConsent.xaml

아래 xaml코드를 입력하면 디자인 화면에 위 그림과 비슷한 모양을 보실 수 있습니다.

<UserControl
    x:Class="CustomControlSample.UserConsent"
    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:local="clr-namespace:CustomControlSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="200"
    d:DesignWidth="300"
    mc:Ignorable="d">
    <Grid Background="{StaticResource {x:Static SystemColors.ControlBrushKey}}">
        <StackPanel Margin="4" HorizontalAlignment="Center">
            <TextBlock TextWrapping="Wrap">
                애플리케이션 사용 동의<LineBreak /><LineBreak />
                이 애플리케이션을 사용하기 위해서는 아래 항목에 동의를 해야합니다.<LineBreak />
                1.정의.....</TextBlock>
            <CheckBox Margin="0,10,0,0" IsChecked="{Binding IsUserConsent, Mode=TwoWay}">동의 합니다.</CheckBox>
            <StackPanel
                Margin="0,40,0,0"
                HorizontalAlignment="Center"
                Orientation="Horizontal">
                <Button Width="80" Command="{Binding SubmitCommand}">제출</Button>
                <Button
                    Width="80"
                    Margin="10,0,0,0"
                    Command="{Binding ExitCommand}">
                    종료
                </Button>
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

UserConsentViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Windows.Input;

namespace CustomControlSample
{
    /// <summary>
    /// 사용자 동의 뷰모델
    /// </summary>
    public class UserConsentViewModel : ObservableObject
    {
        /// <summary>
        /// 서비스 프로바이더
        /// </summary>
        private readonly IServiceProvider _serviceProvider;
        private bool _isUserConsent;
        /// <summary>
        /// 사용자 동의 여부
        /// </summary>
        public bool IsUserConsent
        {
            get => _isUserConsent;
            set => SetProperty(_isUserConsent, value,
                            callback =>
                            {
                                _isUserConsent = callback;
                                ((RelayCommand)SubmitCommand).NotifyCanExecuteChanged();
                            });
        }
        /// <summary>
        /// 제출 커맨드
        /// </summary>
        public ICommand SubmitCommand { get; set; }
        /// <summary>
        /// 팝업 닫기 커맨드 - MainWindowViewModel에서 연결
        /// </summary>
        public ICommand ClosePopupCommand { get; set; }
        /// <summary>
        /// 종료 커맨드
        /// </summary>
        public ICommand ExitCommand { get; set; }
        /// <summary>
        /// 기본 생성자
        /// </summary>
        public UserConsentViewModel()
        {

        }
        /// <summary>
        /// 런타임 생성자
        /// </summary>
        /// <param name="serviceProvider">서비스 프로바이더 주입</param>
        public UserConsentViewModel(IServiceProvider serviceProvider) : this()
        {
            //App.Current.Services.GetService를 직접 사용하지 않고, Injection 받아서 사용합니다.
            _serviceProvider = serviceProvider;
            Init();
        }

        private void Init()
        {
            SubmitCommand = new RelayCommand(() =>
                {
                    ClosePopupCommand?.Execute(true);
                }, () => IsUserConsent);
            ExitCommand = new RelayCommand(() =>
                {
                    ClosePopupCommand?.Execute(false);
                });
        }
    }
}

MainWindow부터 GetService를 이용해서 Injection(주입) 시켜서 실행하기

이전 포스트들에서는 MainWindow는 StartupUri="MainWindow.xaml" 구문을 이용해서 인스턴스 하고, MainWindow.xaml.cs에서 아래 코드를 이용해서 ViewModel을 주입 시켜서 사용했습니다.

DataContext = App.Current.Services.GetService(typeof(MainViewModel));

이번에는 처음부터 모두 GetService를 이용해보겠습니다.

1. App.xaml에서 StartupUri="MainWindow.xaml"을 제거합니다.

2. App.xaml.cs에 아래 메소드를 override하고, ConfigureServices()에 내용을 추가합니다.

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    var mainWindow = Services.GetService<MainWindow>();
    if(mainWindow != null)
    {
        mainWindow.Show();
    }
    else
    {
        Shutdown();
    }
}

/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton(typeof(MainWindow));
    services.AddSingleton(typeof(MainWindowViewModel));

    services.AddTransient(typeof(UserConsent));
    services.AddTransient(typeof(UserConsentViewModel));

    return services.BuildServiceProvider();
}

GetService를 이용해서 주입시키기 위해서는 미리 등록해 두어야 합니다.

3. 이제는 MainWindow.xaml.cs에 추가로 아래 내용을 입력합니다.

MainWindow.xaml이 주입될 때 Injection constructor(주입 생성자)가 실행되고, 그 안에서 ViewModel을 주입 받아서 바로 사용할 수 있습니다.

주의! 주입 생성자 뒤에 기본 생성자 실행 코드 this()를 꼭 추가해 주세요
using System.Windows;

namespace CustomControlSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        public MainWindow(MainWindowViewModel viewModel) : this()
        {
            ViewModel = viewModel;
        }

        public MainWindowViewModel ViewModel
        {
            get =>
                //프로젝트 속성이 null 허용이 아니라 이렇게 처리했습니다
                (MainWindowViewModel)DataContext;
            set => DataContext = value;
        }
    }
}

MainWindowViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Windows.Input;

namespace CustomControlSample
{
    public class MainWindowViewModel : ObservableObject
    {
        private bool _isPopupOpen;
        /// <summary>
        /// 팝업 오픈
        /// </summary>
        public bool IsPopupOpen
        {
            get => _isPopupOpen;
            set => SetProperty(ref _isPopupOpen, value);
        }
        private object? _popupContent;
        /// <summary>
        /// 팝업 컨텐츠
        /// </summary>
        public object? PopupContent
        {
            get => _popupContent;
            set => SetProperty(ref _popupContent, value);
        }
        private string _message;
        /// <summary>
        /// 결과 출력용 메시지
        /// </summary>
        public string Message
        {
            get => _message;
            set => SetProperty(ref _message, value);
        }
        /// <summary>
        /// 서비스 프로바이더
        /// </summary>
        private readonly IServiceProvider _serviceProvider;
        /// <summary>
        /// 팝업 출력 커맨드
        /// </summary>
        public ICommand ShowPopupCommand { get; set; }
        /// <summary>
        /// Popup내부에서 호출할 닫기 커맨드
        /// </summary>
        public ICommand ClosePopupCommand { get; set; }

        public MainWindowViewModel()
        {

        }
        public MainWindowViewModel(IServiceProvider serviceProvider) : this()
        {
            //App.Current.Services.GetService를 직접 사용하지 않고, Injection 받아서 사용합니다.
            _serviceProvider = serviceProvider;

            ShowPopupCommand = new RelayCommand(OnShowPopup);
            ClosePopupCommand = new RelayCommand<bool>(b =>
                {
                    //결과 출력
                    Message = b ? "동의 했습니다." : "동의가 완료되지 않았습니다.";
                    IsPopupOpen = false;
                    PopupContent = null;
                });
        }
        /// <summary>
        /// 팝업 열기
        /// </summary>
        private void OnShowPopup()
        {
            IsPopupOpen = true;
            UserConsent? content = _serviceProvider.GetService<UserConsent>();
            if (content == null)
            {
                return;
            }
            content.Width = 300;
            content.Height = 200;
            if (content.DataContext is UserConsentViewModel viewModel)
            {
                viewModel.ClosePopupCommand = ClosePopupCommand;
            }
            PopupContent = content;
        }
    }
}

실행화면

 

 

소스

WpfTest/CustomControlSample at master · kaki104/WpfTest (github.com)

 

GitHub - kaki104/WpfTest

Contribute to kaki104/WpfTest development by creating an account on GitHub.

github.com

 

반응형
댓글