WPF User Control Dependency Property 오류

안녕하십니까.
제 Applcation에는 8개의 같은 모양의 Toggle button이 존재합니다. User Control로 만들어서 사용중이고, Command와 Command Parameter를 Dependency Property로 만들어서 사용하려고 합니다.
8개의 Toggle button은 하나의 Command를 바인딩 받고 Parameter로 함수의 동작을 나누려고 하는데,
xaml단에서 기본값 형식과 일치하지 않는다고 오류가 발생하네요. 제가 뭘 잘못 했을까요?
image
View

<StackPanel Height="72" Width="auto" Margin="0 -10 0 75" 
            HorizontalAlignment="Center" Orientation="Horizontal">
       <togglebtn:outputbtn Margin="20 30 00 0" 
                            DOCommand="{Binding DOControl}" 
                            DOCommandParameter="{Binding DoNums[0]}"/>
       <togglebtn:outputbtn Margin="0 30 00 0" 
                            DOCommand="{Binding DOControl}" 
                            DOCommandParameter="{Binding DoNums[1]}"/>
       <togglebtn:outputbtn Margin="0 30 00 0" 
                            DOCommand="{Binding DOControl}" 
                            DOCommandParameter="{Binding DoNums[2]}"/>
       <togglebtn:outputbtn Margin="0 30 20 0" 
                            DOCommand="{Binding DOControl}" 
                            DOCommandParameter="{Binding DoNums[3]}"/>
</StackPanel>
<StackPanel Height="40" Margin="0 0 0 0" Width="346"
            Orientation="Horizontal" HorizontalAlignment="Center">
        <TextBlock Text="DO5" Margin="42 10 40 0" Height="auto"/>
        <TextBlock Text="DO6" Margin="15 10 40 0" Height="auto"/>
        <TextBlock Text="DO7" Margin="15 10 40 0" Height="auto"/>
        <TextBlock Text="DO8" Margin="15 10 40 0" Height="auto"/>
  </StackPanel>
  <StackPanel Height="72" Margin="0 61 0 0"
              HorizontalAlignment="Center" Orientation="Horizontal">
        <togglebtn:outputbtn Margin="20 15 00 0" 
                             DOCommand="{Binding DOControl}" 
                             DOCommandParameter="{Binding DoNums[4]}"/>
        <togglebtn:outputbtn Margin="0 15 00 0" 
                             DOCommand="{Binding DOControl}" 
                             DOCommandParameter="{Binding DoNums[5]}"/>
        <togglebtn:outputbtn Margin="0 15 00 0" 
                             DOCommand="{Binding DOControl}" 
                             DOCommandParameter="{Binding DoNums[6]}"/>
        <togglebtn:outputbtn Margin="0 15 20 0" 
                             DOCommand="{Binding DOControl}" 
                             DOCommandParameter="{Binding DoNums[7]}"/>

UserControl.xaml.cs

        public ICommand DOCommand
        {
            get { return (ICommand)GetValue(DOCommandProperty); }
            set { SetValue(DOCommandProperty, value);}
        }

        public static DependencyProperty DOCommandProperty = 
            DependencyProperty.Register(nameof(DOCommand), typeof(ICommand), typeof(outputbtn), new PropertyMetadata(null, RaiseCommandPropertyCallback));

        private static void RaiseCommandPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            outputbtn command = (outputbtn)d;
            ToggleButton btn = command.btnDO;
            btn.Command = e.NewValue as ICommand;
        }

        public int DOCommandParameter
        {
            get { return (int)GetValue(DOCommandParameterProperty); }
            set { SetValue (DOCommandParameterProperty, value); }
        }

        public static DependencyProperty DOCommandParameterProperty =
            DependencyProperty.Register(nameof(DOCommandParameter), typeof(int), typeof(outputbtn), new PropertyMetadata(null, RaiseParameterPropertyCallback));

        private static void RaiseParameterPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            outputbtn patameter = (outputbtn)d;
            ToggleButton btn = patameter.btnDO;
            btn.CommandParameter = int.Parse((string)e.NewValue);
        }

View Model

        private List<int> _donums;

        public List<int> DoNums { get { return _donums; } }


        private RelayCommand<int> _docontrol;

        public ICommand DOControl
        {
            get
            {
                if (_docontrol == null)
                {
                    _docontrol = new RelayCommand<int>(DoOnOff);
                }

                return _docontrol;
            }
        }

        private void DoOnOff(int par)
        {
            MessageBox.Show($"toggle button num : {par}");
        }

        IOPageVM() //ctor
       {
            _donums = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, };
       } 
1 Like

기본값이 null인데요 int에 null을 기본값으로 할 수 없으므로 타입 불일치에서 발생하는 것 같습니다.
0 등으로 바꾸시면 됩니다.

1 Like

네 해결됐습니다. 근데 프로그램을 실행하니 토글 버튼이 클릭이 안되네요. 혹시 이유를 알고 계실까요?

공유된 코드로는 명확히 알 수는 없지만 토글 버튼 이벤트에 의한 동작을 Command로 전달하는 코드가 보이지 않는데요, 아마 그 문제일 듯 합니다.

1 Like

이해가 잘 안가네요. xaml에서 Command를 바인딩 했으니 이벤트를 넘겨 준거 아닌가요?

일부 코드만 공유해 주셔서 잘 모르겠으나, 혹시 해당 View 와 ViewModel 연결은 잘 되어 있을까요? DataContext 지정하는 부분이 안보여서요.

1 Like
    <Page.DataContext>
        <vm:IOPageVM/>
    </Page.DataContext>

위와 같이 해당 View에 ViewModel과 연결 되어 있습니다. 팝업에 올라간 토글 버튼인데, 해당 팝업에 다른 컴포넌트들도 바인딩이 되고 있는데 의존 속성으로 유저 컨트롤 해보는게 처음이라 어렵네요.

아마도 view model 연결은 잘 되었을 거 같긴한데

UserControl.xaml.cs 이라고 표시하신 코드가 outputbtn 을 구현한 코드인 거죠?
그리고 Button 을 상속받아서 구현한 거 맞죠?

그렇다면 outputbtn 에서 click 이벤트를 command 로 연결하는 부분도 구현되어 있나요?

기존 button 의 click 이나 toggle 이벤트가 발생했을 때 사용하는 command 를 함께 구현하거나 연결해주어야 의도한대로 동작할 거 같아요.

그 부분이 구현되어 있지 않은 상태에서 ICommand 속성만 usercontrol 에 추가 구현하면

아무일도 일어나지 않습니다…;;

1 Like

outputbtn UserControl 에서 의존 속성을 제대로 바인딩 했는지 확인해 보세요. 코드가 공유되어 있지 않아 도움 드리기가 좀 힘듭니다. ^^

1 Like
<StackPanel>
        <StackPanel.Resources>
            <Style x:Key="toggleBtn" TargetType="{x:Type ToggleButton}">
                <Setter Property="Margin" Value="10,0"/>
                <Setter Property="Height" Value="30" />
                <Setter Property="Width" Value="60" />
                <Setter Property="BorderBrush" Value="Transparent"/>
                <Setter Property="Content" Value="{DynamicResource Off}"/>
                <Style.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="Content" Value="{DynamicResource On}"/>
                        <Setter Property="Background" Value="{DynamicResource ismouseovercolor}"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="{DynamicResource ismouseovercolor}"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </StackPanel.Resources>

        <ToggleButton x:Name="btnDO" Style="{StaticResource toggleBtn}"/>
    </StackPanel>

위 코드가 outputbtn의 코드입니다. click 이벤트를 command에 연결해야 된다고 하시는게 위의 토글 버튼에 Click = "{binding ~~}"이런 식으로 되야 한다고 말씀하시는건가요?

아, 저도 이저 전체 소스 코드를 따라가면서 확인한 게 아니라
정확히 이거다… 라고 말씀은 못 드리겠고 그냥 제시하신 코드 안에서 추측을 한 건데요.

UserControl.xaml.cs 의 일부만 제시하신 걸로봐서
ToggleButton 을 상속해서 뭔가 재정의 한 건 아닌 거 같군요.

  1. 클릭이 안 된다는 게 어떤 말인지 정확히 잘 모르겠어요
    클릭 버튼이 활성화 안 되어서 누르는 게 불가능하다는 얘기인지, UI 로 클릭은 되는데 이벤트 발생이 안 된다는 건지, 이벤트는 들어오는데 command 호출이 안 된다는 건지…
    정확한 의미를 모르겠네요. 이것부터 정확히 알아야 할 거 같아요.

  2. 그냥 여기서 command 호출이 안 된다는 걸로 가정했을 때 드는 의문인데요. UserControl.xaml.cs에서 별도의 ICommand 속성을 정의하시 이유가 있을까요?
    딱히 지금 사용법이나 연결 구문으로 봤을 때에는 그다지 필요하지 않아보여서요. 어차피 원래 Button 의 속성으로 존재하는 건데 따로 정의하는 이유를 모르겠네요. 특별히 전용 command 를 만들거나 조작하는 구문은 없어보이는 데 맞나요?
    ICommand 가 구현된 대상 객체를 선택하고 싶다면 그냥 DataContext 선택 구문을 Binding 구문에 추가하는 게 더 나을 수도 있겠네요.

  3. 일단 그렇다고 치고, 만약 지금 코드에서 정상적으로 빌드되고 Command 연결이 정상이라고 할 때 디버깅을 통해 어디까지 이벤트 호출이 진행되었는지 확인해보셨는지요?
    사실 디버깅 문제는 이렇게 조각난 코드만 가지고는 알 수 있는 게 별로 없어서 큰 도움은 못 드릴 거 같고, 그냥 추측성 발언 몇 가지들만 왔다갔다 할 수밖에 없거든요
    그 다음부터는 스스로 디버깅해서 찾아내는 방법밖에 없긴해요.

가장 좋은 건 재현가능한 소스코드를 같이 올려주시는 게 가장 좋을 거 같은데요. 지금으로썬 요정도가 최선일 거 같슴다.

3 Likes

이 코드를 참고하시겠어요? UserControl 안의 ToggleButton에 Command을 넘기는 방법을 보여줍니다.

| MainWindow.xaml

<Window
    x:Class="WpfApp41.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:local="clr-namespace:WpfApp41"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <StackPanel Orientation="Vertical">
            <local:CustomToggleButton
                Command="{Binding ToggleCommand}"
                CommandParameter="1"
                Text="1" />
            <local:CustomToggleButton
                Command="{Binding ToggleCommand}"
                CommandParameter="2"
                Text="2" />
            <local:CustomToggleButton
                Command="{Binding ToggleCommand}"
                CommandParameter="3"
                Text="3" />
            <local:CustomToggleButton
                Command="{Binding ToggleCommand}"
                CommandParameter="4"
                Text="4" />
            <local:CustomToggleButton
                Command="{Binding ToggleCommand}"
                CommandParameter="5"
                Text="5" />
        </StackPanel>
    </Grid>
</Window>

| MainWindow.xaml.cs
※ Command를 간단하게 구현하기 위해 별도의 ViewModel은 생략하고 xaml.cs에 Community Toolkit MVVM을 사용하여 Command를 구현 하였습니다.

using CommunityToolkit.Mvvm.Input;

using System.Windows;

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

            DataContext = this;
        }

        [RelayCommand]
        void OnToggle(int num)
        {
            MessageBox.Show(num.ToString());
        }
    }
}

| CustomToggleButton.xaml

<UserControl
    x:Class="WpfApp41.CustomToggleButton"
    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:WpfApp41"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    x:Name="this"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <ToggleButton
            Command="{Binding Command, ElementName=this}"
            CommandParameter="{Binding CommandParameter, ElementName=this}"
            Content="{Binding Text, ElementName=this}" />
    </Grid>
</UserControl>

| CustomToggleButton.xaml.cs

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

namespace WpfApp41;

/// <summary>
/// CustomToggleButton.xaml에 대한 상호 작용 논리
/// </summary>
public partial class CustomToggleButton : UserControl
{
    public readonly static DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(CustomToggleButton), new PropertyMetadata(null));
    public readonly static DependencyProperty CommandParameterProperty = DependencyProperty.Register(nameof(CommandParameter), typeof(int), typeof(CustomToggleButton), new PropertyMetadata(0));
    public readonly static DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(CustomToggleButton), new PropertyMetadata(string.Empty));

    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public int CommandParameter
    {
        get => (int)GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public CustomToggleButton()
    {
        InitializeComponent();
    }
}

| 실행
image

4 Likes

음… 그리구 덧붙이자면

이런 상황에서는 UserControl 보다는 CustomControl 을 사용하는 게 더 나아보임다.

사실 UserControl 이나 CustomControl 이나 view 를 만든다는 차원에서는 큰 차이가 없을 수도 있는데

저는 개인적으로

Layout 을 만드는 view → UserControl
componet 를 만드는 view → CustomControl

이렇게 정의를 하고 사용하는 편이에요.

개별 control 수준의 component 를 만들어 독립적으로 재사용해야하는 상황이라면
CustomControl 로 정의해 구현하는 게 훨씬 더 간단하고 직관적일 수 있고, 테스트하기 용이해져요.
또 독립적으로 구현하기 때문에 DataContext 에 신경쓰지 않고 작성할 수 있구욜. ㅇㅅㅇ!

UserControl 에 component 를 다 때려박고 만들어도 큰 상관은 없는데
component 를 포함하는 UserControl 의 경우 DataContext 에 의존적인 구현이 들어갈 가능성이 있고
디버깅할 때 독립적으로 따라기 어려운 상황이 연출될 수도 있어요.(지금처럼말이죠.)

만약 toggleBtn 을 CustomControl 로 구현했다면
command 연결을 디버깅하고 싶을 때 아예 별도의 프로젝트에 뜯어다가 넣고
거기서 간단한 View 에 엮어서 테스트해볼 수 있죠.(문제를 단순화 해서 찾기 위해서!)

그런데 이렇게 UserControl 내부에 정의해 다른 view 와 섞인 상태로 구현하면
위에서 말한 문제를 단순화하는 방식의 디버깅이 어려워 집니다.

아… 네… 그냥 그렇다구요… =ㅁ=

2 Likes

네. Toggle Button을 상속 받아서 재정의 하지는 않았습니다. 8개의 Toggle Button이 모두 같은 외형과 기능을 제공하기 때문에 한 개의 User Control로 정의하여 사용하려고 합니다.

  1. 프로그램을 실행 시, 클릭이 되는건지는 모르겠지만 토글 버튼이 눌리지 않습니다. 원래는 눌리게 될 경우, 토글 버튼의 이미지가 변해야 하는데 변하지 않습니다.

  2. 별도의 ICommand 속성을 정의한 이유는 그렇게 해야만 토글 버튼의 Check나 UnCheck 같은 클릭 시 발생하는 이벤트를 처리할 수 있을거라고 생각 했기 때문입니다. User Control이 필요한 부분에서 호출 후 이벤트를 정의할 때 Dependency Property로 선언한 속성들만 보여서 그렇게 생각했습니다.
    특별히 전용 command를 만들지는 않습니다. 그저 User Control에서 발생한 이벤트를 View Model 단에서 처리하고 싶을 뿐입니다.

  3. 1번의 이유로 바인딩 오류인지, 클릭 시 이벤트가 어디까지 타는지 확인하지 못하고 있습니다…

재현 가능한 소스 코드라고 하시는게 어느 부분을 말씀하시는지 정확히 모르겠습니다. User Control과 비하인드 코드, View와 View Model의 소스 코드를 모두 올렸는데 다른 부분이 더 필요한가요?
필요한 부분을 말씀 주시면 빠른 시간 내에 올려보겠습니다. 질문이 처음이다 보니 미숙하네요. 답변 감사드립니다.

1 Like

CustomControl의 존재를 UserControl 보다 늦게 알아버렸네요… 다음에는 말씀 해주신 내용 반영해서 개발해보도록 하겠습니다. … 말씀 감사드립니다.

2 Likes

command 가 동작하는 지에 대한 것만 한정해서욤.

  1. 우선 이것부터 확인해야할 거 같아요. 다른 view 에 막혀서 아예 mouse down 이 전달 안 됐을 수도 있어요. 정상적으로 click 이벤트가 발생했는지 확인부터 해야할 거 같습니다.

  2. ToggleButton 이 Checked / Unchecked 이벤트를 따로 지원하긴하는데 view 에서 직접 이벤트 핸들러를 붙여서 사용하는 상황이 아니라면 다른 방법을 주로 사용합니다.
    이미 command 를 붙여서 구현했다는 건 toggle 상태가 변하는 것을 받아서 ViewModel 에서 뭔가 처리하고 싶다는 걸로 보이는데요.
    그럼 단순히 click 이벤트에 대응하는 Command 속성을 연결해 처리하기보다는 IsChecked 속성을 binding 해 ViewModel 에서 직접 받아서 사용하는 게 더 나은 선택 같아요.(저라면 그렇게 하겠숨…)

  3. 이벤트를 확인하는 방법은 VS의 visual tree 나… 저는 snoop 애용합니다…(옛날 사람… ;ㅅ;)

재현가능한 소스 코드는 말 그대로 vs 로 열었을 때 로딩 가능한 수준 혹은 적어도 @dimohy 의 예제 정도 수준을 얘기합니닷 ㅇㅅㅇ/

2 Likes

화면마다 동일한 기능을 하는 control이 필요해서 유저컨트롤로 만들었는데 각 화면하다 처리를 어찌하나 고민했는데,… 감사합니다~!!

1 Like