DataTemplate 내부에서 RelativeSource AncestorType을 잘 찾게하는 방법 질문

  1. 원하는 동작
    DataTemplate 내부에 만들어 둔 Popup.Combobox에 ItemsSource가 Ancestor를 잘 찾아가서 바인딩되길 원합니다.

  2. 시도한 방법
    RelativeSource AncestorType, AncestorLevel을 전부 지정해줬습니다.

  3. 문제점
    goodWorkListview부분은 DataTemplate 내부에서 직접 템플릿을 구현하였습니다. local:PlaceData 데이터가 들어올 경우 DataTemplate이 동작하여 Combobox가 원하는 정상 동작을 하게됩니다.
    notGoodWorkListView 부분은 역시 DataTemplate을 사용하고 있지만, 다르게 접근하고 있습니다.
    local:PlaceData 데이터가 들어올 경우 단순히 토글 버튼만 표시됩니다.
    토글 버튼을 누를 경우 Popup이 활성화되며 Tag에 넣어둔 내용물이 표현되도록 만들었습니다.
    여기서 문제를 하나 발견했습니다.
    Combobox의 ItemsSource를 찾지 못하고있습니다.
    VisualStudio에서 프로젝트 빌드 후 시작하여 토글버튼을 누르면 Combobox가 비어있는것을 확인할 수 있습니다.
    이 상태에서 Combobox의 ItemsSource 코드를 지웠다가 다시 적으면 그때부턴 잘 동작하게 됩니다.

  4. 의심가는 부분
    notGoodListView의 내부에서 DataTemplate을 설정한 후 다시 한번 Tag에 집어넣게 되면서 DataContext가 꼬인게 아닐까 의심이 가지만 어떻게 접근, 수정을 해야하는지 잘 모르겠습니다.

MainWindow.xaml 입니다.

<Window Name="myTestWindow" x:Class="RelativeSourceErrorTest.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RelativeSourceErrorTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>

    </Window.Resources>
    <Grid DataContext="{Binding ElementName=myTestWindow}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <ListView Name="goodWorkListView" ItemsSource="{Binding PlaceDatas}">
            <ListView.ItemTemplate>
                <DataTemplate DataType="{x:Type local:PlaceData}">
                    <Grid Width="200">
                        <Grid.Resources>
                            <Style TargetType="Border">
                                <Setter Property="Margin" Value="2"/>
                                <Setter Property="BorderBrush" Value="Red"/>
                                <Setter Property="BorderThickness" Value="1"/>
                            </Style>
                            <Style TargetType="TextBlock">
                                <Setter Property="VerticalAlignment" Value="Center"/>
                                <Setter Property="HorizontalAlignment" Value="Center"/>
                            </Style>
                        </Grid.Resources>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="40"/>
                            <ColumnDefinition Width="40"/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <Border Grid.Column="0">
                            <TextBlock Text="{Binding Width}"/>
                        </Border>
                        <Border Grid.Column="1">
                            <TextBlock Text="{Binding Height}"/>
                        </Border>
                        <Border Grid.Column="2">
                            <ComboBox ItemsSource="{Binding PositionSource, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"
                                      SelectedItem="{Binding Position}" HorizontalContentAlignment="Center"/>
                        </Border>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <ListView Name="notGoodWorkListView" Grid.Column="1" ItemsSource="{Binding PlaceDatas}">
            <ListView.ItemTemplate>
                <DataTemplate DataType="{x:Type local:PlaceData}">
                    <Grid Width="200">
                        <ToggleButton Content="Item">
                            <ToggleButton.Tag>
                                <Grid Width="200"  Background="#888888" Margin="5 2">
                                    <Grid.Resources>
                                        <Style TargetType="Border">
                                            <Setter Property="Margin" Value="2"/>
                                            <Setter Property="BorderBrush" Value="Red"/>
                                            <Setter Property="BorderThickness" Value="1"/>
                                        </Style>
                                        <Style TargetType="TextBlock">
                                            <Setter Property="VerticalAlignment" Value="Center"/>
                                            <Setter Property="HorizontalAlignment" Value="Center"/>
                                        </Style>
                                    </Grid.Resources>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="40"/>
                                        <ColumnDefinition Width="40"/>
                                        <ColumnDefinition/>
                                    </Grid.ColumnDefinitions>
                                    <Border Grid.Column="0">
                                        <TextBlock Text="{Binding Width}"/>
                                    </Border>
                                    <Border Grid.Column="1">
                                        <TextBlock Text="{Binding Height}"/>
                                    </Border>
                                    <Border Grid.Column="2">
                                        <ComboBox ItemsSource="{Binding PositionSource, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}, AncestorLevel=1}}"
                                      SelectedItem="{Binding Position}" HorizontalContentAlignment="Center"/>
                                    </Border>
                                </Grid>
                            </ToggleButton.Tag>
                            <ToggleButton.Style>
                                <Style TargetType="ToggleButton">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                <Border Background="{TemplateBinding Background}">
                                                    <Grid>
                                                        <TextBlock Text="{TemplateBinding Content}" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="10" TextAlignment="Center"/>
                                                        <Popup Placement="Bottom" AllowsTransparency="true" IsOpen="{Binding IsChecked , RelativeSource={RelativeSource TemplatedParent}}">
                                                            <Border>
                                                                <ContentPresenter Content="{TemplateBinding Tag}" />
                                                            </Border>
                                                        </Popup>
                                                    </Grid>
                                                </Border>
                                                <ControlTemplate.Triggers>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Background" Value="Pink"/>
                                                    </Trigger>
                                                    <Trigger Property="IsChecked" Value="True">
                                                        <Setter Property="Background" Value="Gray"/>
                                                    </Trigger>
                                                </ControlTemplate.Triggers>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </ToggleButton.Style>
                        </ToggleButton>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

MainWindow.xaml.cs입니다.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace RelativeSourceErrorTest
{
    /// <summary>
    /// MainWindow.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        public bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;

            field = value;
            RaiseOnPropertyChanged(propertyName);
            return true;
        }
        private void RaiseOnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        #endregion

        private Array positionSource;
        private ObservableCollection<PlaceData> placeDatas;

        public Array PositionSource { get => positionSource; set => Set(ref positionSource, value); }

        public ObservableCollection<PlaceData> PlaceDatas { get => placeDatas; set => Set(ref placeDatas, value); }
        public MainWindow()
        {
            PositionSource = Enum.GetValues(typeof(PositionConstants));
            InitializeComponent();
            PlaceDatas = new ObservableCollection<PlaceData>();
            for (int i = 0; i < 10; i++)
            {
                var pos = PositionConstants.Top;
                if (i > 3 && i < 6)
                {
                    pos = PositionConstants.Mid;
                }
                else if (i >= 6)
                {
                    pos = PositionConstants.Bottom;
                }
                PlaceDatas.Add(new PlaceData(pos, i));
            }
        }
    }
    public enum PositionConstants
    {
        Top,
        Mid,
        Bottom
    }
    public class PlaceData : INotifyPropertyChanged
    {
        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        public bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;

            field = value;
            RaiseOnPropertyChanged(propertyName);
            return true;
        }
        private void RaiseOnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        #endregion

        private PositionConstants position;
        public PositionConstants Position { get => position; set => Set(ref position, value); }
        public int Width { get; set; }
        public int Height { get; set; }
        public PlaceData(PositionConstants position, int i)
        {
            Position = position;
            Width = i;
            Height = i;
        }
    }
}

class PlaceData 내부에 들어가게 될 정보의 양이 무수히 많아질 예정입니다.
굳이 DataTemplate내부에서 PopUp을 사용한 이유는 아래와 같습니다.
연관성있는 부분만 UI에서 Popup으로 묶어 View를 간단하게 하는것이 목적입니다.

PositionSource라는 Array를 class PlaceData 내부가 아닌 Window의 behind에서 만든 이유는 PlaceDatas의 수가 많아질 경우 불필요한 배열이 계속 생성되기 때문입니다. 한번 만들어둔 PositionSource를 ItemsSource에 바인딩 해두면 재사용이 가능할것이라 판단했습니다.

문의 주신 내용은 RelativeSource와는 크게 상관이 없는 걸로 보입니다.

위 코드의 문제점은

MainWindow.xaml 파일에 정의된 XAML 코드의 내용은 MainWindow.cs 코드에서

public MainWindow()
{
    PositionSource = Enum.GetValues(typeof(PositionConstants));
    InitializeComponent();
    PlaceDatas = new ObservableCollection<PlaceData>();
    ...
}

MainWindow 객체가 인스턴스화 될 때 생성자 내의 InitializeComponent() 함수의 호출에 의해 형상화 됩니다. XAML 형식으로 정의한 리소스 생성과 및 컨트롤 구성, 속성 지정과 바인딩 등의 동작이 이 시점에 일어나게 됩니다.

InitializeComponent() 호출 시점에 PlaceDatas 속성의 값은 null이기 때문에 XAML 코드 내 바인딩은 실패하게 됩니다. 해당 속성은InitializeComponent() 호출 직후에 할당되지만 ‘PlaceDatas’ 속성은 속성 변경을 통지하지 않는 일반 속성이므로 WPF의 Binding 시스템은 해당 속성이 변경된 것을 알아채지 못합니다.

해결 방법은 간단합니다.

private ObservableCollection<PlaceData> placeDatas = new(); // or new ObservableCollection<PlaceData>();

MainWindow() 생성자 함수 내부의 내용이 실행되기 전에 필드를 초기화 시켜버리면 됩니다. (MainWindow()PlaceDatas 생성 부분은 삭제)

이 부분이 동작하는 이유는 이 시점에서는 PlaceDatas 속성 값은 생성되어 있는 상태인데 XAML Hot Reload 기능에 의해

notGoodWorkListView.SetBinding(ItemsSourceProperty, new Binding("PlaceDatas"));

위 코드와 동일하게 동작하는 바인딩 구성이 재수행 되었기 때문입니다.

WPF에서 바인딩을 사용할 때는 아래 두 가지를 항상 신경쓰셔야 합니다.

  • 바인딩이 구성되는 시점에 속성 값은 무엇인가?
  • 바인딩에 사용된 속성의 변경을 바인딩에 통지해 줄 수 있는가?

아 코드가 잘려서 못봤는데 다시 보니 INotifyPropertyChanged setter를 구현하셨네요 :sweat_smile::sweat_smile:

위 답글은 무시하고, 시간 날 때 다시 답변해드리겠습니다.

  1. placeDatas를 필드부분에서 초기화 진행.
  2. InitializeComponent를 제일 밑으로 내렸습니다.
    하지만 여전히 동작하지 않습니다. 뭔가 다른 문제가 있는것같습니다.
public partial class MainWindow : Window, INotifyPropertyChanged
    {
        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        public bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;

            field = value;
            RaiseOnPropertyChanged(propertyName);
            return true;
        }
        private void RaiseOnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        #endregion

        private Array positionSource;
        private ObservableCollection<PlaceData> placeDatas = new ObservableCollection<PlaceData>();

        public Array PositionSource { get => positionSource; set => Set(ref positionSource, value); }

        public ObservableCollection<PlaceData> PlaceDatas { get => placeDatas; set => Set(ref placeDatas, value); }
        public MainWindow()
        {
            PositionSource = Enum.GetValues(typeof(PositionConstants));

            // PlaceDatas 채우기
            for (int i = 0; i < 10; i++)
            {
                var pos = PositionConstants.Top;
                if (i > 3 && i < 6)
                {
                    pos = PositionConstants.Mid;
                }
                else if (i >= 6)
                {
                    pos = PositionConstants.Bottom;
                }
                PlaceDatas.Add(new PlaceData(pos, i));
            }

            InitializeComponent();
        }
    }

여러가지 방법을 더 시도해봤지만 원인을 찾아내기 참 어렵습니다. ㅠㅠ

  1. DependencyProperty를 이용해봤습니다.
    여전히 동일한 문제가 발생합니다.
  public Array PositionSource_byDP
        {
            get { return (Array)GetValue(PositionSource_byDPProperty); }
            set { SetValue(PositionSource_byDPProperty, value); }
        }

        public static readonly DependencyProperty PositionSource_byDPProperty = DependencyProperty.Register("PositionSource_byDP", typeof(Array), typeof(MainWindow), new PropertyMetadata());
  1. Window가 Load된 후 소스를 다시 생성해봤습니다.
    여전히 동작하지 않습니다.
 private void myTestWindow_Loaded(object sender, RoutedEventArgs e)
        {
            PositionSource = Enum.GetValues(typeof(PositionConstants));
            PositionSource_byDP = Enum.GetValues(typeof(PositionConstants));
        }

저도 비슷한 방법으로 Image의 source를 tag로 받아서 상태에 따라 이미지가 변경되는 형식으로 개발했다가 동일한 오류가 발생한 적 있는데요.

그때 스택오버플로우에서 본 내용은 Tag로 내용을 넘기는 것이 형태가 정확하게 지정되지 않아서 그랬던 것으로 기억합니다.
저는 Image 관련된 프로젝트여서 Tag사용하지 않고 Converter 사용해서 해결했던 기억이 나네요.

어쨌든 핵심은 Tag로 특정 값을 넘겼을 때 형태가 정확하게 지정되지 않아서 정상적으로 넘어가지 않은 것이었던거 같아요. 링크 찾으면 남겨드릴게요

시간 관계상 원인에 대해 먼저 설명드리면 PopupChild 속성으로 지정된 부분은 일반적인 WPF 컨트롤의 Content 속성과 달리, 별도의 운영체제 레벨의 Tool Window에 해당 내용을 띄우는 것으로 Popup 컨트롤이 속해있는 Visual Tree에서 분리되게 됩니다.

Popup.cs - githubcom/dotnet/wpf

보시는 바와 같이 myTestWindow 창 내 Visual Tree에 속한 ToggleButtonTag 속성으로 있던 ComboBoxPopupRoot라는 별도 창의 Visual Tree 하위 요소로 위치 하게 됩니다.

따라서 RelativeSource 탐색 시 Popup 외부로는 더 이상 올라갈 수 없기 때문에 바인딩에 실패하게 됩니다.

사실 UI 구성요소가 Tag 속성으로 존재하는 것도 조금 잘못된 접근입니다. 전반적인 구조 개선이 필요할 것으로 보입니다.

방향에 대한 답변은 시간 될 때 또 올려드리겠습니다.

해당 내용에 대해서 저도 한 번 찾아보도록 하겠습니다.
감사합니다

기존에 Tag에 구성하였던 부분을 Content로 변경했습니다.
일단 notGoodWorkListView를 통한 Popup 내부 Combobox의 정상 동작을 확인했습니다.
다만 방식을 변경하게 되며 토글버튼의 내부 텍스트가 사라지게 되는 현상이 발생했습니다.
내부 텍스트를 변경하기 위해선 PlaceData 내부에 ButtonText 프로퍼티를 따로 넣는 방식을 적용해야 할 것 같습니다.

//내용추가

  1. ToggleButton을 이용한 Popup출력을 하나의 스타일로 변경
  2. ToggleButton에 직접 스타일을 적용할 경우엔 Tag에 버튼텍스트 입력
  3. DataTemplate을 사용해야 할 경우 Data안에 버튼텍스트 프로퍼티 추가하여 사용
 <ListView Name="notGoodWorkListView" Grid.Column="1" ItemsSource="{Binding PlaceDatas}">
            <ListView.ItemTemplate>
                <DataTemplate DataType="{x:Type local:PlaceData}">
                    <Grid Width="200">
                        <ToggleButton Tag="ButtonTextHere">
                            <ToggleButton.Content>
                                <Grid Width="200"  Background="#888888" Margin="5 2">
                                    <Grid.Resources>
                                        <Style TargetType="Border">
                                            <Setter Property="Margin" Value="2"/>
                                            <Setter Property="BorderBrush" Value="Red"/>
                                            <Setter Property="BorderThickness" Value="1"/>
                                        </Style>
                                        <Style TargetType="TextBlock">
                                            <Setter Property="VerticalAlignment" Value="Center"/>
                                            <Setter Property="HorizontalAlignment" Value="Center"/>
                                        </Style>
                                    </Grid.Resources>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="40"/>
                                        <ColumnDefinition Width="40"/>
                                        <ColumnDefinition/>
                                    </Grid.ColumnDefinitions>
                                    <Border Grid.Column="0">
                                        <TextBlock Text="{Binding Width}"/>
                                    </Border>
                                    <Border Grid.Column="1">
                                        <TextBlock Text="{Binding Height}"/>
                                    </Border>
                                    <Border Grid.Column="2">
                                        <ComboBox ItemsSource="{Binding PositionSource_byDP, ElementName=myTestWindow}" 
                                                  SelectedItem="{Binding Position}" HorizontalContentAlignment="Center"/>
                                    </Border>
                                </Grid>
                            </ToggleButton.Content>
                            <ToggleButton.Style>
                                <Style TargetType="ToggleButton">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                <Border Background="{TemplateBinding Background}">
                                                    <Grid>
                                                        <TextBlock Text="{TemplateBinding Tag}" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="10" TextAlignment="Center"/>
                                                        <Popup Placement="Bottom" AllowsTransparency="true" IsOpen="{Binding IsChecked , RelativeSource={RelativeSource TemplatedParent}}">
                                                            <Border>
                                                                <ContentPresenter Content="{TemplateBinding Content}" />
                                                            </Border>
                                                        </Popup>
                                                    </Grid>
                                                </Border>
                                                <ControlTemplate.Triggers>
                                                    <Trigger Property="IsMouseOver" Value="True">
                                                        <Setter Property="Background" Value="Pink"/>
                                                    </Trigger>
                                                    <Trigger Property="IsChecked" Value="True">
                                                        <Setter Property="Background" Value="Gray"/>
                                                    </Trigger>
                                                </ControlTemplate.Triggers>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </ToggleButton.Style>
                        </ToggleButton>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>