I have a TreeView that has 3 levels in it, driven by a HierarchicalDataTemplate. Here is a partial xaml listing:
<TreeView
Name="TreeView"
ItemsSource="{Binding GTOs}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<!-- This binding prevents the TreeView from slipping over to the right when an item is clicked. -->
<StackPanel MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter, AncestorLevel=1}}" />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:TreeGTOViewModel}" ItemsSource="{Binding Children}">
<TextBlock
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeView}}, Path=ActualWidth}"
MouseDown="TextBlock_MouseDown"
Tag="{Binding Uri}"
Text="{Binding Title}" />
</HierarchicalDataTemplate>
The Binding on the Width attribute of the TextBlock insures that each TreeViewItem assumes the full width of the TreeView container (and not just the width of the text box). Coupled with the Material Design styling, this produces nice shading effects when the mouse is hovered over a particular item in the tree.
There's only one problem. While I can get the Width to assume the full width of the TreeViewItem, I can't seem to do the same for the Height. This means that the user can click near the edge of the TreeView item and see all of the effects as if that item was clicked, but the MouseDown event does not fire.
This is the area that should be clickable (the dotted red line):
This is the area that is actually clickable (the outside border of the Text Box):
I've tried a number of different approaches, including this one:
VerticalAlignment="Stretch"
and this one:
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeViewItem}}, Path=ActualHeight}"
but they either have no effect, or they break the TreeView in unexpected ways. I think part of the problem is the usual height weirdness of the StackPanel that is being used in the ItemsPanelTemplate.
How do I get the TextBox in each TreeViewItem to assume the height of the actual TreeViewItem?
Below is an image of the relevant part of the visual tree, from the TreeView down to the clickable Text Box. The Ripple element is a MaterialDesignThemes.Wpf.Ripple object.
I made a very simple MahApp test project with a treeview and one treeviewitem (no binding).
In the Live Visual Tree, the PART_Header had a VerticalAlignment value of Center. If I changed it to Stretch (using Live Property Explorer), the textblock filled the space vertically.
So, I used the Document Outline in VS to edit a copy of the treeview template. Inside of that, I saw <Setter Property="VerticalContentAlignment" Value="Center"> so I decided to set it to Stretch on my TreeView and it seemed to work.
<TreeView x:Name="TreeView"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
VerticalContentAlignment="Stretch">
Well, I solved this by going a different route. Instead of hooking the MouseDown event on the Text Block, I'm going to hook the SelectedItemChanged event on the Tree View.
<TreeView
ItemsSource="{Binding GTOs}"
SelectedItemChanged="TreeView_SelectedItemChanged">
The RoutedPropertyChangedEventArgs of the SelectedItemChanged event contains the ViewModel for that particular TreeViewItem, which yields the Uri property I need for navigation (I was previously binding this property to the TextBox's Tag property).
The new Event Handler looks like this:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
dynamic viewModel = e.NewValue;
var uri = viewModel.Uri;
(DataContext as TreeViewModel).Navigate(uri);
}
Each level of the tree contains a different ViewModel type, but they all have a Uri property, so the dynamic binding provides the needed "don't care what type it is" behavior.
I can now remove the Tag binding and MouseDown event from the TextBlock.
<HierarchicalDataTemplate DataType="{x:Type local:TreeGTOViewModel}" ItemsSource="{Binding Children}">
<TextBlock
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeView}}, Path=ActualWidth}"
Text="{Binding Title}" />
and the Tree View items now respond properly, no matter where they are clicked.
Related
Trying to set up a WPF ComboBox;
some of its items should not be selectable, so I'm binding IsEnabled to some property of the underlying item.
At the same time, I need to define an ItemTemplate that contains e.g. a Button.
This button needs to be clickable, even if the item is not selectable (worth nothing a click on the button should not select the item as such of course; it will trigger a command performing some background actions, which will eventually make the underlying item selectable)
However, when ComboBoxItem.IsEnabled = false, it seems even the button automatically gets disabled.
Brief example:
<ComboBox ItemsSource="{Binding Items}">
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsEnabled" Value="{Binding CanSelectItem}"/>
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<!-- This button isn't clickable when ComboBoxItem.IsEnabled = false .. but it should be! -->
<Button Content="Click me" Command="{Binding SomeCmd}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Is there any way to circumvent this? E.g., set some items as non-selectable, however define a button in the ItemTemplate that remains clickable regardless?
Thanks
When you remove the ComboBox.ItemContainerStyle you can set the IsEnabled property of each element in the DataTemplate
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
IsEnabled="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.IsEnabled}"/>
<Button Content="Click me" Command="{Binding SomeCmd}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
The combobox item will still be enabled but the TextBlock will be disabled.
The binding is just an example. Depends on where your IsEnabled property is. In my example the property is in the viewmodel which is DataContext of your Window.
For future reference, have found another way of doing this, based on this answer:
WPF override IsEnabled from Parent
So basically creating a class derived from button that overrides the default IsEnabled behavior.
Benefit is that this seems to do exactly what I was looking for, but it does change one of WPF's pretty.. default behaviors, so might need to be taken with a bit of care
I have a Window that has a UserControl. What i want to accomplish is Bind the ActualHeight of the StackPanel (stackpanel doesn't have a fixed size) that contains the UserControl in order to achive the correct behavior of the WrapPanel inside the UserControl.
I've tried Height="{Binding ElementName=MyStackPanel, Path=ActualHeight}" and Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type StackPanel}}, Path=ActualHeight}" but it didn't worked.
The way i place the UserControl inside the Window's StackPanel is as follows:
<StackPanel x:Name="MyStackPanel" Orientation="Vertical">
<!-- Wrap Panel / UserControl-->
<local:WrapControl />
</StackPanel>
If it helps the Main Window is a devexpress DXRibbonWindow.
Ok so it seems that StackPanel has no concept of "stretching" to occupy all the space available.
Replacing the StackPanel with a Grid works perfect.
My WPF Windows contains a TabControl which displays content on different tabs. A click on the button below executes a method via ICommand interface / Binding. The called method generates text which is intended to be displayed in the second tab.
How can I switch to the second tab on button click without violating the MVVM Pattern?
I tried to bind the TabItem.IsSelected Property to something in my ViewModel but I wanted to use the other tabs (tab1) as well.
Any thoughts?
I found it out by myself.
The key is a two way binding. When the button is clicked it sets the property DisplayXamlTab true. The IsSelected attribute is bound to this variable. if another tab is clicked the binding will set the DisplayXamlTab Property to false.
Note: UpdateSourceTrigger=PropertyChanged is also very important
Code comes below:
XAML:
<TabItem Header="XAML" IsSelected="{Binding DisplayXamlTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<Grid Background="#FFE5E5E5">
<TextBox x:Name="TxtXamlOutput" IsReadOnly="True" Text="{Binding XamlText, Mode=TwoWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible"/>
</Grid>
</TabItem>
C# Property:
private bool displayXamlTab;
public bool DisplayXamlTab
{
get { return this.displayXamlTab; }
set
{
this.displayXamlTab = value;
this.RaisePropertyChanged("DisplayXamlTab");
}
}
if you're going the MVVM way you're going to create two dependency properties in the code behind:
ObservableCollection<ItemType> Items;
ItemType MySelectedItem;
Then, bind the TabControl ItemsSource property to the Items and bind the SelectedItem property to MySelectedItem
<TabControl ItemsSource="{Binding Items}"
SelectedItem="{Binding MySelectedItem, Mode=TwoWay}">
<TabControl.ItemTemplate>
<DataTemplate>
<... here goes the UI to display ItemType ... >
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
When you want to change the selected tab, simply update the MySelectedItem dependecy property
Although this question is fairly old and well answered already, I thought I'd add this additional answer to demonstrate an alternative way of changing the selected TabItem in a TabControl. If you have a view model for each TabItem, then it can be helpful to have an IsSelected property in it to determine whether it is selected or not. It is possible to data bind this IsSelected property with the TabItem.IsSelected property using the ItemContainerStyle property:
<TabControl ItemsSource="{Binding MenuItems}" TabStripPlacement="Top">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type ControlViewModels:MenuItemViewModel}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageSource}" Margin="0,0,10,0" />
<TextBlock Text="{Binding HeaderText}" FontSize="16" />
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type ControlViewModels:MenuItemViewModel}">
<ContentControl Content="{Binding ViewModel}" />
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
You can now change the selected TabItem from the parent view model like this:
MenuItems[0].IsSelected = true;
Note that because this property is data bound to the TabItem.IsSelected property, calling this...:
MenuItems[1].IsSelected = true;
... will infact also automatically set the MenuItems[0].IsSelected property to false. so if the view model that you are working with has its IsSelected property set to true, then you can be sure that its related view is selected in the TabControl.
You can create a binding between the view model and the TabControl.SelectedIndex property - i.e., 0 selects the first TabItem , 1 selects the second, etc.
<TabControl DataContext="..." SelectedIndex="{Binding SomeVmProperty}" ...
(alternatively, depending on how you've got things set up, you could bind against SelectedItem...)
You'll likely want to use some sort of "Event Aggregator" pattern (I.e. the Messenger class in MVVM Light) to broadcast some sort of "navigation" message. Your View - the TabControl - can listen for the specific message, and navigate to Tab2 when the message is received.
Alternatively, you can bind the "SelectedItem" property of the TabControl to your ViewModel, and simply call CurrentTab = MySecondTabViewModel from within your VM. This is the approach recommended by #HighPoint in the comments to the OP, but I'm not a fan; see below. Another caveat to this approach is that you need to be familiar with DataTemplates, as you will need to map a view to each ViewModel which you display.
I personally like the first approach, because I don't consider it to be a "responsibility" of the ViewModel to handle tab navigation. If you simply alert your View when data changes in your ViewModel, you allow the View to decide whether or not it wants to change tabs.
I'm interested in creating an app that displays some buttons and changes a viewport according to the selected button. The viewport in my app is a ContentControl and I thought of changing its content whenever a button is clicked. However, I believe there's a better approach, by perhaps injecting the ViewModels of each of the Views I want to present to the ContentControl and styling them using DataTemplates (Since I want to avoid having a grid with many controls and just setting their Visibility property whenever I want to show a particular view). Which of the approaches seems better to you? Do you have a different approach for this?
The view should be something similar to this:
Thanks!
Usually have a ViewModel behind the window which contains:
ObservableCollection<IViewModel> AvailableViewModels
IViewModel SelectedViewModel
ICommand SetCurrentViewModelCommand
I display the AvailableViewModels using an ItemsControl, which has its ItemTemplate set to a Button. The Button.Command is bound to the SetCurrentViewModelCommand, and it passes the current data item from the AvailableViewModels collection in through the CommandParameter
To display the content area, I use a ContentControl with ContentControl.Content bound to SelectedViewModel, and DataTemplates get used to tell WPF how to render each ViewModel.
The end result is my XAML looks something like this:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ViewModelA}">
<local:ViewA />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModelB}">
<local:ViewB />
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding AvailableViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.SetCurrentViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ContentControl Content="{Binding SelectedViewModel}" />
</DockPanel>
You can view an example of the full code used for such a setup on my blog
I have this ComboBox in my Silverlight UserControl:
<ComboBox
AutomationProperties.AutomationId="cmbProjects"
Grid.Row="0"
Grid.Column="2"
ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<TextBlock Foreground="DarkRed" AutomationProperties.AutomationId="{Binding Number}" Width="100" Margin="0" Text="{Binding Number, Converter={StaticResource StringFormatter},ConverterParameter='\{0\}'}" />
<TextBlock AutomationProperties.AutomationId="{Binding Description}" Text="{Binding Description, Converter={StaticResource StringFormatter},ConverterParameter='\{0\} '}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The lenghth of most of the items populating the combobox exceeds the width of the control. When I dropdown the list, the dropdown expands, but not fully to the width of the item content, resulting in content that is clipped a horizontal scrollbar. This does not happen with exact same combobox where content is within the original width of the control.
In WPF, I could simply set the width of the item container to auto; in Silverlight this results in a catastrophic error. I can set the with to a huge number, but the scroll still appears, regardless of the width. Also, in Silverlight 2 beta 2 there was a property DropDownWidth, with one of the options being "Auto", which I don't see in RTM.
I can get around this with a bit of trickery, mainly hiding the horizontal scrollbar and appending a bunch of characters so that the dropdown fully expand to show the item content. Obviously, this hack not ideal. Did anyone experience similar problem? Is there something that I'm missing to force the combobox to expand fully without a scrollbar?
ib.
It appears that they fixed it in SL3.
If you want to tweak the PopUp, you can do that from within the ComboBox's Control Template. In Blend follow these steps:
Right Click on ComboBox
Select "Edit Control Parts (Template)"
Select "Edit a Copy"
This will copy the out of the box control style & template so that you can tweak that ScrollViewer inside the ComboBox's PopUp to your heart's content.
You may want to try the solution I describe here. It details how to ensure that the combobox pop-up's height and width are updated when items are added or removed.
You can use MaxDropDownHeight property of ComboBox control.