how to know which treeview item is clicked using mvvm - wpf

I am having a WPF MVVM application which has a TreeView with all the static items maintained in XAML page. How do I know in my view-model which MenuItem is clicked so that I can show that respective page accordingly.
<TreeView Height="Auto" HorizontalAlignment="Stretch" Margin="0" Name="MyTreeViewMenu"
VerticalAlignment="Stretch" Width="Auto" Opacity="1"
BorderThickness="1" BorderBrush="Black" Grid.Row="2">
<TreeViewItem Header="Country" Width="Auto" HorizontalAlignment="Stretch"
></TreeViewItem>
<TreeViewItem Header="View Details" Width="Auto" HorizontalAlignment="Stretch" IsEnabled="False">
<TreeViewItem Header="User" />
<TreeViewItem Header="Group" />
<TreeViewItem Header="User Group" />
</TreeViewItem>
</TreeView>

I suppose that Selected event will have same effect as a click in your case. To determine which one TreeViewItem was selected you should add event Trigger:
<TreeView Height="Auto" HorizontalAlignment="Stretch" Margin="0" Name="MyTreeViewMenu"
VerticalAlignment="Stretch" Width="Auto" Opacity="1"
BorderThickness="1" BorderBrush="Black" Grid.Row="2">
<TreeViewItem Header="Country" Width="Auto" HorizontalAlignment="Stretch"></TreeViewItem>
<TreeViewItem Header="View Details" Width="Auto" HorizontalAlignment="Stretch" IsEnabled="False">
<TreeViewItem Header="User" />
<TreeViewItem Header="Group" />
<TreeViewItem Header="User Group" />
</TreeViewItem>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction
Command="{Binding selectItemCommand}"
CommandParameter="{Binding SelectedItem, ElementName=MyTreeViewMenu}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
As a result you can use and determine which item was selected by a parameter passed to Command.
ViewModel should look something like this:
private ICommand _selectItemCommand;
public ICommand selectItemCommand
{
get
{
return _selectItemCommand ?? (_selectItemCommand = new RelayCommand(param => this.LoadPage(param)));
}
}
private void LoadPage(object selectedMenuItem)
{
...
}

Take a look at the TreeView.SelectedItem Property page at MSDN.
You can bind directly to the TreeView.SelectedItem property:
<TreeView ItemsSource="{Binding Items}" SelectedItem="{Binding Item, Mode=OneWay}" />
Note that the TreeView.SelectedItem property is only read only, so you must use a OneWay binding... this means that you cannot set the selected item from your view model. To do that, you will need to create your own two way selected item property using an Attached Property.
EDIT >>>
My apologies #Scroog1, I normally use an AttachedProperty to do this. You are right that even with a OneWay binding, there is an error using this method. Unfortuately, my AttachedProperty code is long, but there is another way to do this.
I wouldn't necessarily recommend this as it's never really a good idea to put UI properties into your data objects, but if you add an IsSelected property to your data object, then you can bind it directly to the TreeViewItem.IsSelected property:
<TreeView ItemsSource="Items" HorizontalAlignment="Stretch" ... Name="MyTreeViewMenu">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
I just searched and found a 'fuller' answer for you in the WPF MVVM TreeView SelectedItem post here on StackOverflow.
Alternatively, there is another way... you could also use the TreeView.SelectedValue and TreeView.SelectedValuePath properties. The basic idea is to set the TreeView.SelectedValuePath property to the name of a property on your data object. When an item is selected, the TreeView.SelectedValue property will then be set to the value of that property of the selected data item. You can find out more about this method from the How to: Use SelectedValue, SelectedValuePath, and SelectedItem page at MSDN. This generally works best if you have a uniquely identifiable property like an identifier of some kind. This code example is from MSDN:
<TreeView ItemsSource="{Binding Source={StaticResource myEmployeeData},
XPath=EmployeeInfo}" Name="myTreeView" SelectedValuePath="EmployeeNumber" />
<TextBlock Margin="10">SelectedValuePath: </TextBlock>
<TextBlock Margin="10,0,0,0" Text="{Binding ElementName=myTreeView,
Path=SelectedValuePath}" Foreground="Blue"/>
<TextBlock Margin="10">SelectedValue: </TextBlock>
<TextBlock Margin="10,0,0,0" Text="{Binding ElementName=myTreeView,
Path=SelectedValue}" Foreground="Blue"/>

In addition to binding to the TreeView.SelectedItem property:
When using MVVM it helped me to stop thinking about events in the UI and start thinking about state in the UI.
You can bind the ViewModel to properties of the View. So in general I try to bind a SelectedItem to a property on the ViewModel so the ViewModel knows what is selected.
In the same way you could add a property to the ViewModel items being shown called Selected and bind this property to a checkbox in the View. That way you can enable multiple selection and access the selected items easily within the ViewModel.

For completeness, here are the attached property and TreeView subclass options:
Attached property option
public static class TreeViewSelectedItemHelper
{
public static readonly DependencyProperty BindableSelectedItemProperty
= DependencyProperty.RegisterAttached(
"BindableSelectedItem",
typeof (object),
typeof (TreeViewSelectedItemHelper),
new FrameworkPropertyMetadata(false,
OnSelectedItemPropertyChanged)
{
BindsTwoWayByDefault = true
});
public static object GetBindableSelectedItem(TreeView treeView)
{
return treeView.GetValue(BindableSelectedItemProperty);
}
public static void SetBindableSelectedItem(
TreeView treeView,
object selectedItem)
{
treeView.SetValue(BindableSelectedItemProperty, selectedItem);
}
private static void OnSelectedItemPropertyChanged(
DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
var treeView = sender as TreeView;
if (treeView == null) return;
SetBindableSelectedItem(treeView, args.NewValue);
treeView.SelectedItemChanged -= HandleSelectedItemChanged;
treeView.SelectedItemChanged += HandleSelectedItemChanged;
if (args.OldValue != args.NewValue)
SetSelected(treeView, args.NewValue);
}
private static void SetSelected(ItemsControl treeViewItem,
object itemToSelect)
{
foreach (var item in treeViewItem.Items)
{
var generator = treeViewItem.ItemContainerGenerator;
var child = (TreeViewItem) generator.ContainerFromItem(item);
if (child == null) continue;
child.IsSelected = (item == itemToSelect);
if (child.HasItems) SetSelected(child, itemToSelect);
}
}
private static void HandleSelectedItemChanged(
object sender,
RoutedPropertyChangedEventArgs<object> args)
{
if (args.NewValue is TreeViewItem) return;
var treeView = sender as TreeView;
if (treeView == null) return;
var binding = BindingOperations.GetBindingExpression(treeView,
BindableSelectedItemProperty);
if (binding == null) return;
var propertyName = binding.ParentBinding.Path.Path;
var property = binding.DataItem.GetType().GetProperty(propertyName);
if (property != null)
property.SetValue(binding.DataItem, treeView.SelectedItem, null);
}
}
Subclass option
public class BindableTreeView : TreeView
{
public BindableTreeView()
{
SelectedItemChanged += HandleSelectedItemChanged;
}
public static readonly DependencyProperty BindableSelectedItemProperty =
DependencyProperty.Register(
"BindableSelectedItem",
typeof (object),
typeof (BindableTreeView),
new FrameworkPropertyMetadata(
default(object),
OnBindableSelectedItemChanged) {BindsTwoWayByDefault = true});
public object BindableSelectedItem
{
get { return GetValue(BindableSelectedItemProperty); }
set { SetValue(BindableSelectedItemProperty, value); }
}
private static void OnBindableSelectedItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var treeView = d as TreeView;
if (treeView != null) SetSelected(treeView, e.NewValue);
}
private static void SetSelected(ItemsControl treeViewItem,
object itemToSelect)
{
foreach (var item in treeViewItem.Items)
{
var generator = treeViewItem.ItemContainerGenerator;
var child = (TreeViewItem) generator.ContainerFromItem(item);
if (child == null) continue;
child.IsSelected = (item == itemToSelect);
if (child.HasItems) SetSelected(child, itemToSelect);
}
}
private void HandleSelectedItemChanged(
object sender,
RoutedPropertyChangedEventArgs<object> e)
{
SetValue(BindableSelectedItemProperty, SelectedItem);
}
}

Related

WPF TargetUpdated event not firing on ContentControl

I'm using an attached property to subscrive to the TargetUpdated event from a TextBlock, so I can be notified every time the text changes.
Using the following XAML:
<DataTemplate DataType="{x:Type targetUpdatedApp:Item}">
<targetUpdatedApp:LabelControl Text="{Binding Text, NotifyOnTargetUpdated=True}" Style="{StaticResource LabelTemplateStyle}"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{Binding Items}">
<!--<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text, NotifyOnTargetUpdated=True}" targetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</DataTemplate>
</ListBox.ItemTemplate>-->
</ListBox>
<Button Click="ButtonBase_OnClick">Button</Button>
</StackPanel>
Here is my AttachedProperty code:
public class DesiredWidth
{
public static readonly DependencyProperty DesiredMinWidthProperty =
DependencyProperty.RegisterAttached(
"DesiredMinWidth", typeof (double),
typeof (TextBlock), new PropertyMetadata(OnDesiredMinWidthChanged));
public static double GetDesiredMinWidth(DependencyObject obj)
{
return (double) obj.GetValue(DesiredMinWidthProperty);
}
public static void SetDesiredMinWidth(DependencyObject obj, double value)
{
obj.SetValue(DesiredMinWidthProperty, value);
}
static void OnDesiredMinWidthChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var textBlock = obj as TextBlock;
if (textBlock == null)
{
return;
}
if (args.NewValue != null)
{
textBlock.TargetUpdated += OnTextBoxTargetUpdated;
}
}
static void OnTextBoxTargetUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
{
if (e.Property == TextBlock.TextProperty)
{
}
}
}
The Items collection binded to the ListBox is an ObservableCollection and the items on it implement INotifyPropertyChanged.
If I uncomment the code of the ListBox.ItemTemplate and use it instead of the style it works ok, but I use LabelControl (which basically has a Text DependencyProperty) described on the style the TargetUpdated event subscribed on the AttachedProperty never gets fired.
Could someone give me some help on this issue?
Thanks in advance.
PS: Added from comment :
<Style x:Key="LabelTemplateStyle" TargetType="{x:Type argetUpdatedApp:LabelControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type targetUpdatedApp:LabelControl}">
<TextBlock Text="{TemplateBinding Text}" TargetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
NotifyOnTargetUpdated will only apply to the binding that it's set on, not bindings on elements contained within. You have this set on the LabelControl, but the event handler is attached to the TextBlock inside its ControlTemplate, which isn't notifying. Use:
<ControlTemplate TargetType="{x:Type targetUpdatedApp:LabelControl}">
<TextBlock Text="{TemplateBinding Text, NotifyOnTargetUpdated=True}" targetUpdatedApp:DesiredWidth.DesiredMinWidth="120"/>
</ControlTemplate>
Another way you could do this, which wouldn't require you to edit bindings, is to use another attached property instead of the element's own Text property:
<TextBlock DesiredWidth.Text="{Binding Text}" DesiredWidth.MinWidth="120" />
And in DesiredWidth you could then add a OnTextChanged callback that passes the value on to the TextBlock's Text and does any other handling needed.

Access property of object in View from ViewModel

I have a TabControl in my View with multiple TabItems. I would like to change the its IsSelected property of one of the TabItems from my ViewModel.
Here is the xaml code for the View:
<TabControl Height="50" Margin="12,0,0,0">
<TabItem Name="tiCaptureSetup" >
<TabItem.Header>
<Button Name="btnCaptureSetup"
Grid.Column="0"
Width="90"
Height="40"
Margin="5"
ToolTip="Capture Setup"
Content="Capture Setup"
Click="btnCaptureSetup_Click"
IsEnabled="{Binding Path=CaptureSetupButtonStatus, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
IsDefault="True"
></Button>
</TabItem.Header>
</TabItem>
Here is the C# code behind in View
private void btnCaptureSetup_Click(object sender, RoutedEventArgs e)
{
tiCaptureSetup.IsSelected = true; //select Capture Setup TabItem
MenuLSViewModel vm = (MenuLSViewModel)this.DataContext;
if (vm != null)
{
vm.CaptureSetupCommand.Execute(null);
}
}
And I would like to change tiCaptureSetup.IsSelected from ViewModel.
Any suggestions?
Simplest way: Make a property in your ViewModel called something like IsCaptureSetupSelected and bind it to the IsSelected property of tiCaptureSetup.
ViewModel:
private bool _IsCaptureSetupSelected;
public bool IsCaptureSetupSelected
{
get { return _IsCaptureSetupSelected; }
set
{
if (_IsCaptureSetupSelected != value)
{
_IsCaptureSetupSelected = value;
RaisePropertyChanged();
}
}
}
XAML:
<TabItem Name="tiCaptureSetup" IsSelected="{Binding IsCaptureSetupSelected}">
Note that I'm assuming you're using something like MVVMLight with your ViewModel...

WPF RadioButtons in details UserControl unchecked when navigating through parent ListView

---edited to include full code sample---
I know there have been a lot of issues with the .NET 3.5 RadioButton, but I believe this is a different issue that I have not found anything on in my searching. I've put together a very minimal code sample to demonstrate the problem I'm observing.
I have a ListView in my XAML that has its SelectedItem bound to the SelectedModel in my ViewModel. This same XAML contains a StackPanel that shows RadioButton options for the SelectedModel when navigating through items in the ListView. Everything works great until I add a RadioButton group into the mix. Now, if I select a record in the ListView that has one of the RadioButton options checked, then select another record in the ListView that has a different option checked, and now select the previous record in the ListView again, all of the RadioButtons for that ListView item will be unchecked. If I move up and down through the ListView long enough, every RadioButton, and the underlying Boolean values, will be set to false.
Model code:
public class Model
{
public string Name { get; set; }
public bool IsOne { get; set; }
public bool IsTwo { get; set; }
public bool IsThree { get; set; }
}
ViewModel code:
public class ViewModel: INotifyPropertyChanged
{
private Model _selectedModel;
public event PropertyChangedEventHandler PropertyChanged;
public List<Model> ModelList { get; set; }
public Model SelectedModel
{
get { return _selectedModel; }
set
{
_selectedModel = value;
OnPropertyChanged("SelectedModel");
}
}
public ViewModel()
{
ModelList = new List<Model>();
ModelList.Add(new Model() {Name = "Florida"});
ModelList.Add(new Model() {Name = "Texas"});
ModelList.Add(new Model() {Name = "Arizona"});
ModelList.Add(new Model() {Name = "Washington"});
}
void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(
this, new PropertyChangedEventArgs(propName));
}
}
And finally, the XAML code:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding Path=ModelList}"
SelectedItem = "{Binding SelectedModel}"
DisplayMemberPath="Model">
<ListView.View>
<GridView>
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Name}"
Width="100"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Column="1">
<RadioButton x:Name="IsOne" Content="One" Margin="3"
IsChecked="{Binding Path=SelectedModel.IsOne}"
GroupName="{Binding Path=SelectedModel.Name}"/>
<RadioButton x:Name="IsTwo" Content="Two" Margin="3"
IsChecked="{Binding Path=SelectedModel.IsTwo}"
GroupName="{Binding Path=SelectedModel.Name}" />
<RadioButton x:Name="IsThree" Content="Three" Margin="3"
IsChecked="{Binding Path=SelectedModel.IsThree}"
GroupName="{Binding Path=SelectedModel.Name}" />
</StackPanel>
</Grid>
Any ideas of what would be causing the binding to behave in the manner?
Thanks,
Glenn
If you have a number of RadioButtons in each row of your ListView, you will need to provide a unique GroupName for each set of RadioButtons in each row. The only way that I know how to do that is if you add an extra property to the data type class that you are displaying in the GridView. It doesn't matter what that value is as long as each item in the collection has a different value.
You can then Bind the new item property to the GroupName property of each RadioButton in the set:
<StackPanel>
<RadioButton Content="Demo" GroupName="{Binding GroupName}" ... />
<RadioButton Content="Screener" GroupName="{Binding GroupName}" ... />
<RadioButton Content="Vote" GroupName="{Binding GroupName}" ... />
<RadioButton Content="Vote Follow Up" GroupName="{Binding GroupName}" ... />
<RadioButton Content="Named Vote" GroupName="{Binding GroupName}" ... />
</StackPanel>
I found an article that demonstrates this nicely with XAML examples:
Grouped RadioButton for WPF Datagrid
UPDATE >>>
Yep, in your situation, when you have a group of RadioButtons in a container panel of some kind, you don't even need to supply a GroupName as the Framework does that for us 'under the covers'. I refactored your XAML to use a different Binding to see if that made any difference, but it didn't. You've found a bone fide bug there. Here is the XAML that I ended with that still exhibits the same problem:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding ModelList}" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding SelectedModel}" DisplayMemberPath="Model">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="100"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Column="1">
<RadioButton x:Name="IsOne" Content="One" Margin="3" IsChecked="{Binding ModelList/IsOne}" />
<RadioButton x:Name="IsTwo" Content="Two" Margin="3" IsChecked="{Binding ModelList/IsTwo}" />
<RadioButton x:Name="IsThree" Content="Three" Margin="3" IsChecked="{Binding ModelList/IsThree}" />
</StackPanel>
</Grid>
I removed the unused properties from your code example in your question. I would also suggest that you edit your question with a more accurate description of the problem... although I didn't quite work out the pattern of clicks that it would take to uncheck a CheckBox, I could see that it has nothing to do with the third RadioButton in particular. It certainly happened on the other two as well.
It might be worth reporting this as a bug on the Microsoft Connect website... perhaps there's an explanation or workaround there already?
I have confirmed that this is a known issue that has continued into Framework 4.5. Franklin Chen provided me with a nice little workaround that extends the RadioButton class.
Here is a link to his answer.
Here is his code. Now just bind to the IsCheckedReal property and everything works.
public class RadioButtonExtended : RadioButton
{
static bool m_bIsChanging = false;
public RadioButtonExtended()
{
this.Checked += new RoutedEventHandler(RadioButtonExtended_Checked);
this.Unchecked += new RoutedEventHandler(RadioButtonExtended_Unchecked);
}
void RadioButtonExtended_Unchecked(object sender, RoutedEventArgs e)
{
if (!m_bIsChanging)
this.IsCheckedReal = false;
}
void RadioButtonExtended_Checked(object sender, RoutedEventArgs e)
{
if (!m_bIsChanging)
this.IsCheckedReal = true;
}
public bool? IsCheckedReal
{
get { return (bool?)GetValue(IsCheckedRealProperty); }
set
{
SetValue(IsCheckedRealProperty, value);
}
}
// Using a DependencyProperty as the backing store for IsCheckedReal. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsCheckedRealProperty =
DependencyProperty.Register("IsCheckedReal", typeof(bool?), typeof(RadioButtonExtended),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Journal |
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
IsCheckedRealChanged));
public static void IsCheckedRealChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
m_bIsChanging = true;
((RadioButtonExtended)d).IsChecked = (bool)e.NewValue;
m_bIsChanging = false;
}
}

wpf Usercontrol template

In my MVVM app I have a treeview representing records in a database. My views and viewmodels are linked in a resource dictionary like this
<DataTemplate DataType="{x:Type vm:TrialSiteViewModel}">
<vw:TrialSiteView />
</DataTemplate>
I want to display a preview of the view when a user hovers over an icon using the tooltip. My HierarchicalDataTemplate in the treeview is this
<HierarchicalDataTemplate DataType="{x:Type vm:TrialSiteViewModel}"
ItemsSource="{Binding Path=Children}">
...
<Button Style="{StaticResource previewButtonStyle}">
<Button.ToolTip>
<ToolTip Style="{x:Null}">
<ToolTip.ContentTemplate>
<DataTemplate>
<localtools:ObjectPreview
PreviewObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=DataContext}"
/>
</DataTemplate>
</ToolTip.ContentTemplate>
</ToolTip>
</Button.ToolTip>
</Button>
</StackPanel>
</HierarchicalDataTemplate>
This correctly picks up the TrialSiteViewModel that is the DataContext for the Treeviewitem.
ObjectPreview uses a viewbox and contentcontrol to display the view of the record
<Viewbox Grid.Row="1" Name="treeviewViewBox"
Stretch="Uniform"
IsEnabled="False">
<ContentControl Name="treeViewItemViewModel"
Content="{Binding PreviewObject}">
</ContentControl>
</Viewbox>
and the code behind contains the dependency property
public partial class ObjectPreview : UserControl
{
public ObjectPreview()
{
InitializeComponent();
}
public static readonly DependencyProperty _previewObjectProperty =
DependencyProperty.Register("PreviewObject", typeof(TreeViewItemViewModel), typeof(ObjectPreview));
public TreeViewItemViewModel PreviewObject
{
get { return (TreeViewItemViewModel)GetValue(_previewObjectProperty); }
set { SetValue(_previewObjectProperty, value); }
}
}
The problem I'm having is that the Template used to display the object is the same as that used in the treeview. This simply shows an icon and an object summary (ie. Primary Key and one or two key fields) rather than the entire template as defined in the view TrialSiteView. If I amend the code to use a button Command on the TrialSiteViewModel and inject it into ObjectPreview I can set the contentcontrol in the code behind and the TrialSiteView is used.
I'm guessing that somehow the Template is inferred from the TreeViewItem. Can anyone tell me how I can ensure the tooltip uses the TrialSiteView?
UPDATE
Ok, so I've fixed this but had to resort to code behind and removed the usercontrol and put the view directly in the tooltip. The key bit is getting the datatemplate from the resources. I'd tried to do this previously by assigning a key to the datatemplate, but either my code was flawed or it did not work. Anyhow, this works but is not the preferred Xaml solution.
private void PreviewObject_MouseEnter(object sender, MouseEventArgs e)
{
Image image = (Image)sender;
var key = new System.Windows.DataTemplateKey(image.DataContext.GetType());
var datatemplate = (DataTemplate)this.FindResource(key);
ToolTip tooltip = new ToolTip();
tooltip.Style = VisualUtils.GetResource<Style>("ControlTemplates.xaml", "toolTipWithContentStyle");
tooltip.MaxWidth = 460;
ContentControl contentcontrol = new ContentControl();
contentcontrol.ContentTemplate = datatemplate;
contentcontrol.Content = image.DataContext as TreeViewItemViewModel;
Viewbox viewbox = new Viewbox();
viewbox.Stretch = Stretch.Uniform;
viewbox.Child = contentcontrol;
tooltip.Content = viewbox;
image.ToolTip = tooltip;
}
What you need to do is to specify explicitly what data template to use. In order to do that just add a template property along with the PreviewObject property in the preview control:
public static readonly DependencyProperty _previewObjectTemplateProperty =
DependencyProperty.Register("PreviewObjectTemplate", typeof(DataTemplate), typeof(ObjectPreview));
public DataTemplate PreviewObjectTemplate
{
get { return (DataTemplate)GetValue(_previewObjectTemplateProperty); }
set { SetValue(_previewObjectTemplateProperty, value); }
}
Then, in the ObjectPreview.xaml add the ContentTemplate property that is bound to the PreviewObjectTemplate property:
<Viewbox Grid.Row="1" Name="treeviewViewBox"
Stretch="Uniform"
IsEnabled="False">
<ContentControl Name="treeViewItemViewModel"
Content="{Binding PreviewObject}"
ContentTemplate="{Binding PreviewObjectTemplate}" >
</ContentControl>
</Viewbox>
And finally, give a key to your data template and specify a reference to it explicitly when you declare ObjectPreview:
<DataTemplate x:Key="FullViewTemplate" DataType="{x:Type vm:TrialSiteViewModel}">
<vw:TrialSiteView />
</DataTemplate>
...
<ToolTip Style="{x:Null}">
<ToolTip.ContentTemplate>
<DataTemplate>
<localtools:ObjectPreview
PreviewObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=DataContext}"
PreviewObjectTemplate="{StaticResource FullViewTemplate}"
/>
</DataTemplate>
</ToolTip.ContentTemplate>

WPF ComboBox - Showing something different when selecting a value

What I need to accomplish is a ComboBox that shows People. When you expand the drop-down it shows FirstName and LastName, but when you select a person, the value shown at the combobox should be just the person's first name.
I have the following ItemTemplate:
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
What else should I do to display only the first name when one item is selected?
Thanks!
EDIT
Changed the question slightly: What if I have the person's picture and instead of showing just the first name when a person is selected, I want to show only the picture. In other words, how can I have two separate templates - one for the drop-down and one for the selected item?
Here's the solution:
<ComboBox>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ContentControl x:Name="content" Content="{Binding}" ContentTemplate="{StaticResource ComplexTemplate}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter TargetName="content" Property="ContentTemplate" Value="{StaticResource SimpleTemplate}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Basically, you create one more layer of DataTemplate here. ComboBox'es ItemTemplate always stays the same. But the content inside that template adjusts to the condition you are interested in.
The trick to discriminate dropped-down combobox items against selected-area combobox item is that selected-area is not really enclosed in ComboBoxItem object, it's part of ComboBox control itself. So FindAncestor for ComboBoxItem returns null, which we use in the trigger above.
I got it. I just needed to add the following to my ComboBox:
IsEditable="True" IsReadOnly="True" TextSearch.TextPath="FirstName"
Put a Trigger on the DataTemplate. The trigger should check the IsSelected property (the DataTemplate will need a TargetType set for this to work). If it is selected, you can set the Visibility of your TextBlocks to Collapsed, and set the Visibility of the Image to Visible. Then do the opposite for the case that it is not selected.
Another option is to use ItemTemplateSelector instead of ItemTemplate. I've been using it the following way.
ComboBoxItemTemplateSelector derives from DataTemplateSelector and has two attached properties, SelectedTemplate and DropDownTemplate. Then we set the DataTemplates from Xaml like this
<ComboBox ItemsSource="{Binding Persons}"
ItemTemplateSelector="{StaticResource ComboBoxItemTemplateSelector}">
<ts:ComboBoxItemTemplateSelector.SelectedTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}" />
</DataTemplate>
</ts:ComboBoxItemTemplateSelector.SelectedTemplate>
<ts:ComboBoxItemTemplateSelector.DropDownTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</ts:ComboBoxItemTemplateSelector.DropDownTemplate>
</ComboBox>
In SelectTemplate we check if the current container is wrapped in a ComboBoxItem and if it is, we return the DropDownTemplate. Otherwise we return SelectedTemplate.
public class ComboBoxItemTemplateChooser : DataTemplateSelector
{
#region SelectedTemplate..
#region DropDownTemplate..
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
ComboBox parentComboBox = null;
ComboBoxItem comboBoxItem = container.GetVisualParent<ComboBoxItem>();
if (comboBoxItem == null)
{
parentComboBox = container.GetVisualParent<ComboBox>();
return ComboBoxItemTemplateChooser.GetSelectedTemplate(parentComboBox);
}
parentComboBox = ComboBox.ItemsControlFromItemContainer(comboBoxItem) as ComboBox;
return ComboBoxItemTemplateChooser.GetDropDownTemplate(parentComboBox);
}
}
A small demo project that uses this can be downloaded here: ComboBoxItemTemplateDemo.zip
I also made a short blog-post about this here: Different ComboBox ItemTemplate for dropdown. It also shows the other obvious way of doing the same thing but with properties instead of attached properties in ComboBoxItemTemplateSelector.
Oh, and GetVisualParent. Everyone seems to have their own implementations of this but anyway, here's the one I'm using
public static class DependencyObjectExtensions
{
public static T GetVisualParent<T>(this DependencyObject child) where T : Visual
{
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
}
I used next approach
<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>
And the behavior
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;
}
}
worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want

Resources