Nice programing

WPF ComboBox에서 선택한 항목에 드롭 다운 부분의 항목과 다른 템플릿을 사용할 수 있습니까?

nicepro 2020. 12. 1. 19:47
반응형

WPF ComboBox에서 선택한 항목에 드롭 다운 부분의 항목과 다른 템플릿을 사용할 수 있습니까?


예를 들어 Customer 개체로 채워진 WPF Combobox가 있습니다. DataTemplate이 있습니다.

<DataTemplate DataType="{x:Type MyAssembly:Customer}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" />
        <TextBlock Text="{Binding Address}" />
    </StackPanel>
</DataTemplate>

이렇게하면 ComboBox를 열 때 이름과 그 아래에 주소가있는 여러 고객을 볼 수 있습니다.

하지만 고객을 선택하면 ComboBox에 이름 만 표시하고 싶습니다. 다음과 같은 것 :

<DataTemplate DataType="{x:Type MyAssembly:Customer}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" />
    </StackPanel>
</DataTemplate>

ComboBox에서 선택한 항목에 대해 다른 템플릿을 선택할 수 있습니까?

해결책

답변의 도움으로 다음과 같이 해결했습니다.

<UserControl.Resources>
    <ControlTemplate x:Key="SimpleTemplate">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
        </StackPanel>
    </ControlTemplate>
    <ControlTemplate x:Key="ExtendedTemplate">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
            <TextBlock Text="{Binding Address}" />
        </StackPanel>
    </ControlTemplate>
    <DataTemplate x:Key="CustomerTemplate">
        <Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
                <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>
</UserControl.Resources>

그런 다음 내 ComboBox :

<ComboBox ItemsSource="{Binding Customers}" 
                SelectedItem="{Binding SelectedCustomer}"
                ItemTemplate="{StaticResource CustomerTemplate}" />

작동하는 데 중요한 부분은 Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}"(값이 True가 아니라 x : Null이어야하는 부분)이었습니다.


위에서 언급 한 DataTrigger / Binding 솔루션 사용 문제는 두 가지입니다. 첫 번째는 실제로 선택한 항목에 대한 상대 소스를 찾을 수 없다는 바인딩 경고로 끝납니다. 그러나 더 큰 문제는 데이터 템플릿을 복잡하게 만들고이를 ComboBox에 특정하게 만들었다는 것입니다.

내가 제시하는 솔루션은 WPF 디자인을 더 잘 따르고 있습니다. WPF 디자인 DataTemplateSelectorSelectedItemTemplateDropDownItemsTemplate속성을 사용하여 별도의 템플릿을 지정할 수 있으며 둘 다에 대한 '선택기'변형을 사용할 수 있습니다.

public class ComboBoxTemplateSelector : DataTemplateSelector
{
    public DataTemplate         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector SelectedItemTemplateSelector  { get; set; }
    public DataTemplate         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector DropdownItemsTemplateSelector { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var itemToCheck = container;

        // Search up the visual tree, stopping at either a ComboBox or
        // a ComboBoxItem (or null). This will determine which template to use
        while(itemToCheck != null && !(itemToCheck is ComboBoxItem) && !(itemToCheck is ComboBox))
            itemToCheck = VisualTreeHelper.GetParent(itemToCheck);

        // If you stopped at a ComboBoxItem, you're in the dropdown
        var inDropDown = (itemToCheck is ComboBoxItem);

        return inDropDown
            ? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
            : SelectedItemTemplate  ?? SelectedItemTemplateSelector?.SelectTemplate(item, container); 
    }
}

참고 : 단순성을 위해 여기에있는 예제 코드에서는 새로운 '?.'를 사용합니다. C # 6 (VS 2015)의 기능입니다. 이전 버전을 사용하는 경우 '?'를 제거하기 만하면됩니다. 위의 'SelectTemplate'을 호출하기 전에 명시 적으로 null을 확인하고 그렇지 않으면 다음과 같이 null을 반환합니다.

return inDropDown
    ? DropdownItemsTemplate ??
        ((DropdownItemsTemplateSelector != null)
            ? DropdownItemsTemplateSelector.SelectTemplate(item, container)
            : null)
    : SelectedItemTemplate ??
        ((SelectedItemTemplateSelector != null)
            ? SelectedItemTemplateSelector.SelectTemplate(item, container)
            : null)

또한 XAML에서 편의를 위해 위의 클래스를 간단히 만들고 반환하는 태그 확장도 포함했습니다.

public class ComboBoxTemplateSelectorExtension : MarkupExtension
{
    public DataTemplate         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector SelectedItemTemplateSelector  { get; set; }
    public DataTemplate         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector DropdownItemsTemplateSelector { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new ComboBoxTemplateSelector(){
            SelectedItemTemplate          = SelectedItemTemplate,
            SelectedItemTemplateSelector  = SelectedItemTemplateSelector,
            DropdownItemsTemplate         = DropdownItemsTemplate,
            DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
        };
    }
}

사용 방법은 다음과 같습니다. 멋지고 깨끗하며 명확하며 템플릿이 '순수'상태로 유지됩니다.

참고 : 'is :'는 여기에 클래스를 코드에 넣는 위치에 대한 xmlns 매핑입니다. 자신의 네임 스페이스를 가져 와서 'is :'를 적절하게 변경하십시오.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />

원하는 경우 DataTemplateSelectors를 사용할 수도 있습니다.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

또는 믹스 앤 매치! 여기서는 선택한 항목에 대한 템플릿을 사용하고 있지만 DropDown 항목에는 템플릿 선택기를 사용합니다.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

또한 선택한 항목 또는 드롭 다운 항목에 대해 Template 또는 TemplateSelector를 지정하지 않으면 예상대로 다시 데이터 유형을 기반으로하는 데이터 템플릿의 정규 분석으로 돌아갑니다. 따라서 예를 들어 아래의 경우 선택한 항목에는 템플릿이 명시 적으로 설정되어 있지만 드롭 다운은 데이터 컨텍스트에서 개체의 DataType에 적용되는 데이터 템플릿을 상속합니다.

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MyTemplate} />

즐겨!


간단한 솔루션 :

<DataTemplate>
    <StackPanel>
        <TextBlock Text="{Binding Name}"/>
        <TextBlock Text="{Binding Address}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </StackPanel>
</DataTemplate>

(목록이 아닌 상자에 선택되고 표시되는 요소는 내부에 없으므로 ComboBoxItem에서 트리거됩니다. Null)

당신이 전체 템플릿을 전환하려는 경우, 당신은뿐만 아니라 예에 트리거를 사용하여 그렇게 할 수있는 다른 적용 ContentTemplateA를ContentControl . 이렇게하면 DataType이 선택적 사례에 대한 템플릿 만 변경하는 경우 기본 기반 템플릿 선택 을 유지할 수도 있습니다 . 예 :

<ComboBox.ItemTemplate>
    <DataTemplate>
        <ContentControl Content="{Binding}">
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
                                        Value="{x:Null}">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <!-- ... -->
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>
</ComboBox.ItemTemplate>

이 방법은 선택한 항목에 대한 상대 소스를 찾을 수 없으므로 바인딩 오류를 발생시킵니다. 대체 접근 방식은 MarqueIV의 답변을 참조하십시오 .


제목 선택으로 Text 매개 변수를 사용하여 콤보 항목에 대해 ItemTemplate의 조합을 사용하도록 제안하려고했지만 ComboBox가 Text 매개 변수를 존중하지 않는다는 것을 알았습니다.

ComboBox ControlTemplate을 재정 의하여 비슷한 것을 처리했습니다. 다음 은 .NET 4.0 용 샘플 이있는 MSDN 웹 사이트 입니다.

내 솔루션에서 ComboBox 템플릿의 ContentPresenter를 Text에 바인딩하도록 변경하고 ContentTemplate은 다음과 같이 TextBlock을 포함하는 간단한 DataTemplate에 바인딩합니다.

<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
    <TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>

ControlTemplate에서 다음과 같이하십시오.

<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>

이 바인딩 링크를 사용하면 컨트롤의 Text 매개 변수를 통해 직접 콤보 선택 표시를 제어 할 수 있습니다 (ViewModel의 적절한 값에 바인딩).


다음 접근 방식을 사용했습니다.

 <UserControl.Resources>
    <DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
        <TextBlock Text="{Binding Path=ShortName}" />
    </DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
    <ComboBox DisplayMemberPath="FullName"
              ItemsSource="{Binding Path=Offsets}"
              behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
              SelectedItem="{Binding Path=Selected}" />
    <TextBlock Text="User Time" />
    <TextBlock Text="" />
</StackPanel>

그리고 행동

public static class SelectedItemTemplateBehavior
{
    public static readonly DependencyProperty SelectedItemDataTemplateProperty =
        DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));

    public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
    {
        element.SetValue(SelectedItemDataTemplateProperty, value);
    }

    public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
    {
        return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
    }

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = d as ComboBox;
        if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
        {
            uiElement.Loaded -= UiElementLoaded;
            UpdateSelectionTemplate(uiElement);
            uiElement.Loaded += UiElementLoaded;

        }
    }

    static void UiElementLoaded(object sender, RoutedEventArgs e)
    {
        UpdateSelectionTemplate((ComboBox)sender);
    }

    private static void UpdateSelectionTemplate(ComboBox uiElement)
    {
        var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
        if (contentPresenter == null)
            return;
        var template = uiElement.GetSelectedItemDataTemplate();
        contentPresenter.ContentTemplate = template;
    }


    public static T GetChildOfType<T>(DependencyObject depObj)
        where T : DependencyObject
    {
        if (depObj == null) return null;

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = (child as T) ?? GetChildOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }
}

매력처럼 작동했습니다. 여기에있는 Loaded 이벤트가별로 마음에 들지 않지만 원하는 경우 수정할 수 있습니다.


예. 템플릿 선택기사용하여 런타임에 바인딩 할 템플릿을 결정합니다. 따라서 IsSelected = False이면이 템플릿을 사용하고 IsSelected = True이면이 다른 템플릿을 사용합니다.

참고 : 템플릿 선택기를 구현 한 후에는 템플릿에 키 이름을 제공해야합니다.


In addition to what said by H.B. answer, the Binding Error can be avoided with a Converter. The following example is based from the Solution edited by the OP himself.

The idea is very simple: bind to something that alway exists (Control) and do the relevant check inside the converter. The relevant part of the modified XAML is the following. Please note that Path=IsSelected was never really needed and ComboBoxItem is replaced with Control to avoid the binding errors.

<DataTrigger Binding="{Binding 
    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
    Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
    Value="{x:Null}">
  <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>

The C# Converter code is the following:

public class ComboBoxItemIsSelectedConverter : IValueConverter
{
    private static object _notNull = new object();
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // value is ComboBox when the item is the one in the closed combo
        if (value is ComboBox) return null; 

        // all the other items inside the dropdown will go here
        return _notNull;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

참고URL : https://stackoverflow.com/questions/4672867/can-i-use-a-different-template-for-the-selected-item-in-a-wpf-combobox-than-for

반응형