티스토리 뷰

반응형

 

2022.10.06 - [WPF .NET] - 사용자 정의 컨트롤을 만들기 Custom XAML Control - part1

 

이전 part1에서 만들었던 컨트롤을 Custom Control를 이용해서 다시 만들어 보도록 하겠습니다.

1. CustomControl 추가

솔루션 탐색기 -> 프로젝트 선택 -> 마우스 오른쪽 -> Add -> New Item 

2. CustomControl의 특징

  • Code와 Xaml이 서로 분리되어 있습니다.
    • CustomUserConsent.cs
    •  Themes/Generic.xaml 파일 내부에  Style로 정의된 CustomUserConsent
    • Themes/Generic.xaml 파일은 App 전역 리소스에 자동으로 등록되는 특수한 성격의 ResourceDictionary입니다.
    • ResourceDictionary 내부에 주석 이외의 한글을 입력하면, 컴파일시 오류가 발생할 수 있습니다.
  • 리소스를 최적화해서 메모리 소비, 성능 및 접근성을 향상 시키며, 시각적 일관성을 유지할 수 있습니다.
  • ControlTemplate 내부에 UI 형태를 자유롭게 구성할 수 있습니다.
  • 약간 제한적인 바인딩만 사용 가능합니다.(Dependency Property만 바인딩 가능)
    • TemplateBinding 사용해서 바인딩하는 경우
      • OneWay 바인딩을 사용해야 할 때
    • RelativeSource의 TemplatedParent를 이용해서 바인딩하는 경우
  • Xaml로 만들어진 컨트롤 중 일부를 Code에서 사용하기 위해서는 다음과 같이 처리합니다.
    • Xaml 컨트롤에 x:Name을 이용해서 이름을 지정합니다. 기본적으로 "PART_..."라는 이름으로 시작합니다.
    • CustomUserConsent.cs 상단에 이 컨트롤에서 반드시 필요한 Child 컨트롤들의 정보를 추가합니다.
      • private const string _partSubmit = "PART_Submit"; 
      • [TemplatePart(Name = _partSubmit, Type = typeof(Button))]
    • OnApplyTemplate() 메소드를 override해서 Child 컨트롤을 가져옵니다.
      • _submitButton = GetTemplateChild(_partSubmit) as Button;
    • 이렇게 처리하는 이유는 혹시 이 컨트롤을 상속 받아서 다른 컨트롤을 만들때, ControlTemplate 내부에 반드시 포함되어야 하는 컨트롤들의 정보를 나열하는 것입니다.
Binding RelativeSource={RelativeSource Mode=TemplatedParent}}를 이용해서 Parent를 찾는 방법이 ControlTemplate와 ContentTemplate에서 사용이 가능하며, ContentTemplate에서 사용하는 경우 ContentPresenter를 반환합니다.

3. CustomUserConsent.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace CustomControlSample
{
    /// <summary>
    /// 사용자 동의 - CustomControl
    /// </summary>
    [TemplatePart(Name = _partSubmit, Type = typeof(Button))]
    [TemplatePart(Name = _partExit, Type = typeof(Button))]
    public class CustomUserConsent : Control, IDisposable
    {
        private const string _partSubmit = "PART_Submit";
        private const string _partExit = "PART_Exit";

        private Button? _submitButton;
        private Button? _exitButton;
        private bool disposedValue;

        static CustomUserConsent()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomUserConsent), new FrameworkPropertyMetadata(typeof(CustomUserConsent)));
        }

        public bool IsUserConsent
        {
            get => (bool)GetValue(IsUserConsentProperty);
            set => SetValue(IsUserConsentProperty, value);
        }

        /// <summary>
        /// 사용자 동의 여부 - DP
        /// </summary>
        public static readonly DependencyProperty IsUserConsentProperty =
            DependencyProperty.Register(nameof(IsUserConsent), typeof(bool), typeof(CustomUserConsent), new PropertyMetadata(false));

        /// <summary>
        /// 팝업 닫기 커맨드 - 생성시 할당됨
        /// </summary>
        public ICommand? ClosePopupCommand { get; set; }

        public override void OnApplyTemplate()
        {
            _submitButton = GetTemplateChild(_partSubmit) as Button;
            _exitButton = GetTemplateChild(_partExit) as Button;
            if (_submitButton == null || _exitButton == null)
            {
                throw new NullReferenceException($"{_partSubmit} and {_partExit} button cannot be null");
            }
            _submitButton.Click += SubmitButton_Click;
            _exitButton.Click += ExitButton_Click;
        }

        private void ExitButton_Click(object sender, RoutedEventArgs e)
        {
            if (ClosePopupCommand != null)
            {
                ClosePopupCommand.Execute(false);
            }
        }

        private void SubmitButton_Click(object sender, RoutedEventArgs e)
        {
            if (ClosePopupCommand != null)
            {
                ClosePopupCommand.Execute(IsUserConsent);
            }
        }
        /// <summary>
        /// Dispose pattern
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    if (_submitButton != null)
                    {
                        _submitButton.Click -= SubmitButton_Click;
                        _submitButton = null;
                    }
                    if (_exitButton != null)
                    {
                        _exitButton.Click -= ExitButton_Click;
                        _exitButton = null;
                    }
                }

                // TODO: free unmanaged resources (unmanaged objects) and override finalizer
                // TODO: set large fields to null
                disposedValue = true;
            }
        }

        // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
        // ~CustomUserConsent()
        // {
        //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        //     Dispose(disposing: false);
        // }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

_submitButton, _exitButton 등의 컨트롤을 Code에서 사용하는 이유는, 클릭 이벤트를 핸들링해야 하기 때문입니다.

4. CustomUserConsent Style

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomControlSample">

    <Style TargetType="local:CustomUserConsent">
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Background" Value="White" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CustomUserConsent">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel Margin="4" HorizontalAlignment="Center">
                            <TextBlock TextWrapping="Wrap">
                                Consent to use the application<LineBreak /><LineBreak />
                                In order to use this application, you must agree to the items below.<LineBreak />
                                1. Definition...</TextBlock>
                            <CheckBox
                                x:Name="PART_UserConsent"
                                Margin="0,10,0,0"
                                IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsUserConsent, Mode=TwoWay}">
                                I agree.
                            </CheckBox>
                            <StackPanel
                                Margin="0,40,0,0"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                                <Button
                                    x:Name="PART_Submit"
                                    Width="80"
                                    IsEnabled="{Binding ElementName=PART_UserConsent, Path=IsChecked}">
                                    Submit
                                </Button>
                                <Button
                                    x:Name="PART_Exit"
                                    Width="80"
                                    Margin="10,0,0,0">
                                    Exit
                                </Button>
                            </StackPanel>
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

GetService를 이용해서 Injection을 받기 위해서 App.xaml.cs는 아래와 같이 작업되어있습니다.

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

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

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

    services.AddTransient(typeof(CustomUserConsent));

    return services.BuildServiceProvider();
}

5. 실행

 

 

6. 소스

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

 

GitHub - kaki104/WpfTest

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

github.com

UserControl이나 CustomControl이나 출력 결과는 크게 다르지 않다는 것을 알 수 있습니다. 다만, 만드는 난이도는 약간의 차이를 보이고 있는데, 직접 만들어 보시고, 원하는 것으로 만드시면 될 것 같습니다.

 

반응형
댓글