2011-01-12 7 views
42

Mam combobox WPF, który jest wypełniony, powiedzmy, obiektów klienta. Mam DataTemplate:Czy mogę użyć innego szablonu dla wybranego elementu w zestawie ComboBox WPF niż dla elementów w rozwijanej części?

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

W ten sposób, gdy otwieram moje ComboBox, widzę różnych klientów z podaniem ich nazwy, a poniżej adres.

Ale kiedy wybieram klienta, chcę wyświetlić tylko nazwę w ComboBox. Coś jak:

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

Czy mogę wybrać inny szablon dla wybranego elementu w ComboBox?

Rozwiązanie

z pomocą odpowiedzi, Rozwiązałem to tak:

<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> 

Wtedy, moja ComboBox:

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

Ważną częścią aby zmusić go do pracy był Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (część, w której wartość powinna być x: Null, nie True).

+1

Twoje rozwiązanie działa, ale pojawiają się błędy w oknie Output. 'System.Windows.Błąd danych: 4: Nie można znaleźć źródła dla wiązania z odniesieniem "RelativeSource FindAncestor, AncestorType = 'System.Windows.Controls.ComboBoxItem', AncestorLevel = '1' '. BindingExpression: Path = IsSelected; DataItem = null; elementem docelowym jest 'ContentPresenter' (Name = ''); właściwością target jest "NoTarget" (type 'Object') ' – user2190035

+1

Pamiętam, że widziałem również te błędy. Ale nie jestem już w projekcie (ani nawet w firmie), więc nie mogę tego sprawdzić, przepraszam. – Peter

Odpowiedz

31

Problem z użyciem wspomnianego wyżej rozwiązania DataTrigger/Binding jest dwojaki. Pierwszy z nich kończy się ostrzeżeniem wiążącym, że nie można znaleźć źródła względnego dla wybranego elementu. Większym problemem jest jednak bałagan w szablonach danych i dostosowywanie ich do ComboBox.

Rozwiązanie, które prezentuję lepiej po projektach WPF, polega na tym, że używa DataTemplateSelector, na którym można określić oddzielne szablony przy użyciu właściwości SelectedItemTemplate i DropDownItemsTemplate, a także selektorów dla obu.

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 parent = container; 

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

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

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

Note: For simplicity, my example code here uses the new '?.' feature of C#6 (VS 2015). If you're using an older version, simply remove the '?' and explicitly check for null before calling 'SelectTemplate' above and return null otherwise like so:

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

Mam również rozszerzenie znaczników, które po prostu Tworzy i zwraca powyższą klasę dla wygody w 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 
     }; 
    } 
} 

A oto jak z niego korzystać. Ładne, czyste i jasne i szablony pozostać 'czysta'

Note: 'is:' here is my xmlns mapping for where I put the class in code. Make sure to import your own namespace and change 'is:' as appropriate.

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

Można również użyć DataTemplateSelectors jeśli wolisz ...

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

Albo łączyć! Tutaj używam szablonu dla wybranego elementu, ale selektora szablonu dla elementów DropDown.

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

Dodatkowo, jeśli nie zostanie określony szablon lub TemplateSelector dla wybranych lub rozwijanych elementów, po prostu wraca do regularnego rozwiązywania szablonów danych na podstawie typów danych, znowu, jak można by oczekiwać. Na przykład w poniższym przypadku wybrany element ma swój szablon jawnie ustawiony, ale lista rozwijana odziedziczy dowolny szablon danych dla obiektu DataType obiektu w kontekście danych.

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

Ciesz się!

+0

Bardzo fajnie. I rzeczywiście mam te wiążące ostrzeżenia (nigdy nie dowiedziałem się, skąd pochodzą, ale tak naprawdę nie spojrzałem). Naprawdę mogę to teraz sprawdzić, ale może w przyszłości. – Peter

+0

Cieszę się, że mogę pomóc. Wystarczy wiedzieć, czy używasz tego w swoim kodzie, instrukcja return ('return inDropDown' powyżej) używa nowego C# 6 ?. składnia, więc jeśli nie korzystasz z VS 2015, po prostu usuń "?" i jawnie sprawdzaj wartości zerowe przed wywołaniem 'SelectTemplate'. Dodam to do kodu. – MarqueIV

+3

Odzyskuję czapkę, aby uzyskać rozwiązanie, które naprawdę można ponownie wykorzystać! – henon

0

Tak. Używasz Template Selector do określenia, który szablon powiążesz w czasie wykonywania. Zatem jeśli IsSelected = False, a następnie Użyj tego szablonu, jeśli IsSelected = True, użyj tego innego szablonu.

Uwaga: Po wdrożeniu selektora szablonów należy podać nazwy szablonów.

+0

Próbowałem tego, używając przykładów, które znalazłem tutaj (http://www.developingfor.net/net/dynamically-switch-wpf-datatemplate.html), ale okazało się, że nie są one do wielokrotnego użytku, i chciałem rozwiązać to w XAML tylko. – Peter

28

Proste rozwiązanie:

<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> 

(Zauważ, że element, który jest wybrany i wyświetlane w oknie, a nie lista nie jest wewnątrz ComboBoxItem stąd wyzwalania na Null)

Jeśli chcesz wyłączyć cały szablon, możesz to zrobić, używając spustu do np apply a different ContentTemplate to a ContentControl. To także pozwala zachować domyślny DataType opartych wybór szablonu, jeśli tylko zmienić szablon dla tej selektywnej przypadku np:

<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> 

Należy zauważyć, że metoda ta spowoduje wiążących błędy jako względny źródło nie zostało znalezione dla wybrany przedmiot. Aby uzyskać alternatywne podejście, patrz MarqueIV's answer.

+0

Chciałem użyć dwóch szablonów, aby go oddzielić. Użyłem kodu z przykładowego projektu z tej strony: http://www.developingfor.net/net/dynamically-switch-wpf-datatemplate.html. Ale gdy działał na ListBox, nie działał na ComboBox. Twoje ostatnie zdanie rozwiązało to lub mnie. Wybrany element w ComboBox nie ma IsSelected = True, ale ma wartość Null. Zobacz moją edycję powyżej, aby uzyskać pełny kod, w jaki sposób go rozwiązałem. Wielkie dzięki! – Peter

+0

Cieszę się, że było to przydatne, mimo że nie było to dokładnie to, o co prosiłeś. Nie wiedziałem o zerowej rzeczy, zanim spróbowałem odpowiedzieć na twoje pytanie, eksperymentowałem i dowiedziałem się o tym w ten sposób. –

+4

'IsSelected' nie jest zerowalny i nigdy nie może być naprawdę NULL. Nie potrzebujesz 'Path = IsSelected', ponieważ sprawdzanie NULL dla otaczającego elementu ComboBoxItem jest całkowicie wystarczające. – springy76

1

Chciałem zaproponować użycie kombinacji ItemTemplate dla elementów złożonych, z parametrem Text jako wyborem tytułu, ale widzę, że ComboBox nie respektuje parametru Text.

Zajmowałem się czymś podobnym, zastępując ComboBox ControlTemplate. Oto MSDN website z próbką dla .NET 4.0.

W moim rozwiązanie, zmienić ContentPresenter w szablonie ComboBox mieć to wiązać się z tekstem, z jego ContentTemplate zobowiązany do prostego DataTemplate, który zawiera TextBlock tak:

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

z tym w ControlTemplate :

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

Dzięki tej wiążącej linku, jestem w stanie kontrolować ekran wyboru Combo bezpośrednio poprzez parametr tekstowy kontroli (co wiążę do odpowiedniej wartości na moim ViewModel).

+0

Nie jestem pewien czy tego właśnie szukam. Chcę wygląd ComboBox, który nie jest "aktywny" (tzn. Użytkownik go nie kliknął, nie jest "otwarty"), aby pokazać tylko jeden kawałek tekstu. Ale wtedy, gdy użytkownik kliknie, powinien otworzyć/rozwinąć, a każdy element powinien pokazać dwa fragmenty tekstu (czyli inny szablon). – Peter

+0

Jeśli eksperymentujesz z powyższym kodem, myślę, że dotrzesz tam, gdzie chcesz. Ustawiając ten szablon kontrolny, możesz kontrolować zwinięty tekst kombinacji za pomocą właściwości Text (lub dowolnej właściwości, która Ci się podoba), umożliwiając wyświetlanie prostego, niezaznaczonego tekstu. Możesz modyfikować poszczególne teksty przedmiotów, określając ItemTemplate podczas tworzenia combobox. (Element ItemTemplate będzie prawdopodobnie miał panel Stackpanel i dwa TextBlocks, lub jakakolwiek formuła ci się podoba.) – cunningdave

1

użyłem następny podejście

<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> 

a zachowanie

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; 
    } 
} 

pracował jak czar. Nie podoba mi się prawie Załadowane zdarzenie tutaj, ale możesz to naprawić, jeśli chcesz

Powiązane problemy