[WPF] DataTemplate에서 Custom Control의 Style 상속이 적용되지 않는 문제

안녕하세요.
WPF 개발을 하는 도중 막히는 부분이 있어 질문드립니다.

아래는 제가 겪은 것을 간단하게 만든 샘플예제입니다.

우선 아래와 같이 Button을 상속받은 Custom Control을 만들었습니다.

public TestButton : Button
{
    public TestButton()
    {
        this.Style = Application.Current.TryFindResource("TestButtonStyle") as Style;
    }
}
<Style

    x:Key="TestButtonStyle"

    TargetType="{x:Type local:TestButtonStyle}">

        <Setter Property="BorderBrush" Value="{StaticResource Black}" />

        <Setter Property="BorderThickness" Value="1" />

</Style>

해당하는 Button을 DataTemplate 안에 넣고 사용하려고 아래와 같이 xaml을 구성을 하였습니다.

<Grid>
    <Grid.Resources>
        <DataTemplate x:Key="TestTemplate">
            <local:TestButton x:Name="testButton" Content="Test" >
                <local:TestButton.Style>
                    <Style BasedOn="{StaticResource TestButtonStyle}" TargetType="{x:Type local:TestButton}">
                        <Setter Property="Visibility" Value="Collapsed" />
                    </Style>
                </local:TestButton.Style>
             </local:TestButton>
        </DataTemplate>
    </Grid.Resources>
    <ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource TestTemplate}" />
</Grid>

하지만 Button은 Collapsed가 되지 않았습니다.
아래와 같이 DataTemplate에 넣지 않으니 Collapsed가 잘 되더라구요.

<Grid>
    <local:TestButton x:Name="testButton" Content="Test" >
        <local:TestButton.Style>
            <Style BasedOn="{StaticResource TestButtonStyle}" TargetType="{x:Type local:TestButton}">
                <Setter Property="Visibility" Value="Collapsed" />
            </Style>
        </local:TestButton.Style>
    </local:TestButton>
</Grid>

몇몇 글을 읽어 보았고,
CustomControl을 만들 때 테마에 DefaultStyle을 설정하지 않고 생성자에서 Style을 할당 했기 때문에
DataTemplate에서 해당 컨트롤의 테마 DefaultStyle을 찾지 못해서 그런건가? 하는 의심이 들었습니다.

해결방법은 여러가지가 있을 것 같은데…

근본적인 원인을 모르고 지나가는 것 같아 한 번 여쭤보려고합니다.

혹시 왜 이런 현상이 나는지 설명해 주실 수 있으실까요?

추가적으로 Custom Control을 만들 때 테마의 DefaultStyle을 DefaultStyleKeyProperty로 지정하지 않고 이렇게 생성자에서 Style을 지정한다면 어떤 차이점이 있을까요?(생성자에서 Style을 넣어줫을 때 DefaultStyle이 될 거 같아 위와 같이 컨트롤을 만들었습니다.)

좋아요 2

생성자에 Style을 지정할 경우 <ContentPresenter>에서 <local:TestButton.Style>의 스타일이 적용 안되는 것을 저도 그렇게 확인이 됩니다.

왜 이렇게 동작하는지는 저도 모르겠습니다. 다만, 기본 스타일로 지정하는게 맞으므로 기본 스타일로 지정했을 경우 <ContentPresenter>에서 올바르게 <local:TestButton.Style> 스타일이 적용되는 것을 확인할 수 있었습니다.

...
        <Style
            x:Key="TestButtonStyle"
            BasedOn="{StaticResource {x:Type Button}}"
            TargetType="{x:Type local:TestButton}">
            <Setter Property="Foreground" Value="Red" />
            <Setter Property="BorderBrush" Value="Black" />
            <Setter Property="BorderThickness" Value="1" />
        </Style>
...

: TestButton.cs

public class TestButton : Button
{
    static TestButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(TestButton), new FrameworkPropertyMetadata(typeof(TestButton)));
    }
    public TestButton()
    {
        //this.Style = Application.Current.TryFindResource("TestButtonStyle") as Style;
    }

    protected override void OnStyleChanged(Style oldStyle, Style newStyle)
    {
        base.OnStyleChanged(oldStyle, newStyle);

        ;
    }
}
...
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="TestTemplate">
                <local:TestButton x:Name="testButton" Content="{Binding}">
                    <local:TestButton.Style>
                        <Style BasedOn="{StaticResource TestButtonStyle}" TargetType="{x:Type local:TestButton}">

                            <Setter Property="Visibility" Value="Collapsed" />

                        </Style>
                    </local:TestButton.Style>
                </local:TestButton>
            </DataTemplate>
        </Grid.Resources>

        <ContentPresenter
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Content="Test!!!"
            ContentTemplate="{StaticResource TestTemplate}" />
좋아요 4

답변으로 채택하겠습니다! 늦은 밤에 답변 달아주셔서 정말 감사합니다.

하나만 더 답변 해주실 수 있으실까요? 하나 더 궁금한게 있어서요.

MSDN에서는 DefaultStyleKeyProperty.OverrideMetadata를 하지 않았을 경우 부모 컨트롤의 기본 Style을 따른다고 적혀 있더라구요.

그렇다면 TestButton 또한 Button의 기본 Style을 따르기 때문에 위와 같이 사용했을 때에 괜찮을 거라 생각했었는데 아닌 것 같습니다.

generic.xaml 및 테마를 사용하지 않더라도 DefaultStyleKeyProperty.OverrideMetadata 의 유무가 꼭 필요한가? 에 대해서 많이 와 닿지 않아서요. 염치 없지만 DefaultStyleKeyProperty.OverrideMetadata에 대해 조금만 설명 부탁 드려도 괜찮을까요?

좋아요 2

DefaultStyleKeyProperty.OverrideMetadata()에 대한 부분은 아래의 링크를 참조하시면 좋습니다.

그냥 쉽게 기본 스타일을 지정한다고 이해하시면 좋습니다. 이후에는 기본 키를 지정하지 않더라도 키가 타입인 스타일을 기본 스타일로 사용하게 됩니다.

<Style
    BasedOn="{StaticResource {x:Type Button}}"
    TargetType="{x:Type local:TestButton}">
    <Setter Property="Foreground" Value="Red" />
    <Setter Property="BorderBrush" Value="Black" />
    <Setter Property="BorderThickness" Value="1" />
</Style>

※ 키를 지정하지 않으면 키 이름은 "{x:Type TestButton}"이 됩니다.

이 스타일은 generic.xaml에 위치하지 않아도 되고 app.xaml에 위치해도 됩니다. 특정 경로의 generic.xaml에 위치하는 이유는 테마 시스템을 사용하기 위함입니다.

좋아요 5

일단 예시로 들어주신 style 에 잘못이 있숨다.

여기서 TargetType 이 잘못되었습니다. 이대로 빌드 하면 실패가 날 텐데,
대략 이 TargetType 에 TestButton 을 넣었다고 가정하도 얘기를 해볼게요.


이거 제가 아는데까지 얘기해볼텐데 혹시 잘못된 거 있으면 지적질해주셈뇨.ㅇㅅㅇ+

어음… 그러니까 이건,

결국 타이밍 문제입니다.
아마 디버깅을 안 해보셔서 확인이 안 되었을 걸로 예상되는데

여기서 Application.Current.TryFindResource("TestButtonStyle") as Style; 이게 null 입니다.
이게 null 이라서 정상적으로 style 이 안 먹는 거 처럼 보이는 거예요.
(네. 실제로 안 먹었죠. 그런데 그건 style 이 null 이라서 그런 거예요. 있는 스타일이 적용 안 된 게 아니어요.)

게중에서도 착각하지 말아야하는 게 있는데
xaml 에 작성된 것은 선언된 것이 아니라는 겁니다. 정의되어 있는 겁니닷. =ㅁ=;

그래서 Application.Current.TryFindResource("TestButtonStyle") as Style; 이게 null 을 뱉고
그대로 this.Style 에 할당해버리면 정의되어 있는 애들이 싹 날라가고 null 이 됩니다.
그래서 요기서

아래 요녀석들

<Style BasedOn="{StaticResource TestButtonStyle}" TargetType="{x:Type local:TestButton}">
   <Setter Property="Visibility" Value="Collapsed" />
</Style>

요게 통째로 null 되는 겁니다.


근데 여기서 디버깅을 해보면 한 가지 의문이 더 들거예요. 그게 뭐냐…

this.Style 도 이미 null 이었다.

네. 그렇숩니다. this.Style 도 이미 null 이었숩니다.

어, 그럼 this.Style 은 왜 null 임? xaml 에 정의되어 있는데 왜 null 임?


그렇죠. 정의되어 있죠.
근데 코드가 정의된 거랑 런타임에 실행되는 건 또 다른 겁니다.

그러니까 저 생성자가 호출될 타이밍에는
Application.Current.TryFindResource("TestButtonStyle") 요방식은 아니지만…
어쨌든 마찬가지로 resource 를 뒤져서 style 을 읽어와야하는데
그걸 읽어올 수 없는 타이밍이라서 디버깅 인스턴스에는 null 로 잡히는 겁니다.

그래서 보통 이런식으로 스타일 조정은 생성자가 아니라
컨트롤이 로드되어 비주얼 트리에 편입되는 시점에 조작하는 게 좋습니다.

대표적으로는 OnApplyTemplate() 이 있죠.
(근데 요놈은 Visible이 Collapsed 되거나 View 에서 제거되면 호출이 안 됩니다.)

그게 아니라면 @dimohy 님의 설명처럼 TargetType 을 지정하면 잘 동작합니다.

그리고 보통 custom control 은 Theme의 Generic.xaml 에 정의된
별도의 ControlTemplate 을 이용해 내용을 작성하죠.
Style 도 거기를 이용하는 게 바람직합니다.

맞나요?ㅅ?

좋아요 8

감사합니다.

DefaultStyleKeyProperty.OverrideMetadata() 가 어떤 역할을 하는지 이해가 가네요.

말그대로 커스텀 컨트롤의 기본적인 스타일을 지정하는 것이라고 이해하였습니다.

감사합니다.

좋아요 2

늦은 밤이지만 답변 해 주셔서 감사합니다.

TargerType에 TestButtonStyle는 예시로 만들다보니 실수로 Style을 빼는것을 빠뜨렸나봅니다. 혼란을 드려 죄송합니다.

Datatemplate에만 들어가지않으면 Style은 잘 적용이 되더라구요.

Application.Current.TryFindResource("TestButtonStyle") as Style;
디버깅을 해봤을때 해당 Style도 확인이 되고 StyleChanged도 불리더라구요.

잘생각해보니 아래 말씀처럼 스타일이 null이 아니라도 정의된 애들이 날라가고 해당 Style로 적용이 될거라고 생각이 드네요.

그런데 의문점은 왜 Datatemplate의 Style을 적용한 컨트롤은 날아가버리고 일반적으로 사용했을땐 잘되는지 둘의 차이점이 무엇일까요?

xaml은 정의된 것이다 를 생각해보면 Datatemplate의 정의 시점과 일반적으로 Grid 내에서 사용했을때의 정의 시점이 다른거같아 발생하는것 같긴한데 맞을까요?

좋아요 2

넵. DataTemplate 에 의한 로딩과 직접 control 을 생성하여 로딩하는 것의 차이가 있어요.

DataTemplate 으로 로딩이 되는 건, 상위 context 의 로딩에 의해
resource 를 검색하는 과정이 발생한 후 비주얼 트리에 로드되는 것이고

직접 control 을 정의하여 로딩하면 그 즉시 바로 비주얼 트리에 로드되기 때문에

앞서 언급했던 resource 에서 스타일을 찾을 수 있는 타이밍이 다릅니다.

그래서 Application.Current.TryFindResource("TestButtonStyle") 요것이
DataTemplate 내부의 control 생성자에서는 null 이었다가
직접 생성한 control 에서는 아니었다가 하는 거죠.

좋아요 3

해당 이슈에 대해 어느정도 감이 잡혔습니다.

답변해주셔서 감사합니다.

좋아요 2