WPF - Hovering over one Element shows content in another Element - wpf

I would like to achieve an affect whereby I can hover over a Button and have a TextBlock update its content (via binding). To complicate matters, The Button is one of many buttons defined in an ItemsControl/DataTemplate. The TextBlock is outside the scope of the ItemsControl.
Some simplified markup of the problem is as follows:
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Title}"
Command="{Binding Command}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Name="TitleTextBox" Grid.Row="1" />
</Grid>
Say in this example, I may want to bind the "Title" property of the data item to the TextBlock's "Text" property.
I assume I want to be tapping into the IsMouseOver of the button, but I can't seem to get it hooked up properly.

I don't believe you're going to be able to accomplish this without some code... however, you don't need to use codebehind. A "behavior" would work just fine for this (either an old school attached behavior, or a more modern Blend Behavior). Here's what the modified markup might look like using an attached behavior:
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl x:Name="Buttons" Grid.Row="0" ext:Hover.TrackChildItems="true">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Title}"
Command="{Binding Command}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Name="TitleTextBox"
Grid.Row="1"
Text="{Binding ElementName=Buttons, Path=ext:Hover.Item.Content}" />
</Grid>
Here the attached behavor "Hover.TrackChildItems" is used to watch for the appropriate mouse events, and sets the readonly "Hover.Item" attached property to the control being hovered over. Writing the behavior should be simple enough, and is left to you.
Edit: To set up the event handlers for the items, the first thing to do is simple and obvious: iterate over the items in the Items property and add the handlers.
Mouse.AddMouseEnter(item, OnMouseEnter);
This works fine for static content, but dynamic (added and removed at runtime) content will be missed by this. So, next you must track changes to the Items property.
((INotifyCollectionChanged)Items).CollectionChanged += OnItemsChanged;
Add or remove the mouse event handlers as appropriate when items are added and removed in the CollectionChanged handler.
There's also another solution. Create a new HoverTrackingItemsControl for this, derived from ItemsControl. In this control override the GetContainerForItemOverride method to return a new HoverTrackingItem, which handles MouseEnter/MouseLeave to notify the parent HoverTrackingItemsControl. This solution is probably easier to implement, though it does require a specialized control, while the Behavior solution is more generic and can be used with any ItemsControl type (ItemsControl, ListBox, ComboBox, etc.).

Related

How to bind from outside a tab control into a tab control when using ControlTemplates?

I want to bind from outside a tab control to its inside. It works as long in principle but when I use a ContentTemplate I have no clue how to address the binding target.
The code below has two pairs of TextBlock and TabItem. The first block works, the second, using a ContentTemplate doesn't.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Text="{Binding ElementName=redTtextBlock, Path=Text}"
Foreground="Red"
/>
<TabControl Grid.Row="1">
<TabItem
Header="Red header"
>
<TextBlock
x:Name="redTtextBlock"
Text="Red text"
Foreground="Red"
/>
</TabItem>
</TabControl>
<TextBlock
Grid.Row="2"
Text="{Binding ElementName=greenTtextBlock, Path=Text}"
Foreground="Olive"
/>
<TabControl Grid.Row="3">
<TabControl.Resources>
<DataTemplate x:Key="template" DataType="TabItem">
<TextBlock
x:Name="greenTtextBlock"
Text="Green text"
Foreground="Olive"
/>
</DataTemplate>
</TabControl.Resources>
<TabItem
Header="Green header"
ContentTemplate="{StaticResource template}">
</TabItem>
</TabControl>
</Grid>
You can never bind from outside of any template to its inside. This are totally different scopes.
"How could I have known? Is there an explanation when one can bind to elements together and when not? It comes to me as a surprise that this binding is not possible directly."
You shouldn't be surprised. If you understand how templates work, then you would know that the second binding can't work.
The first example binds to a control where binding target and source share a common namescope. A template e.g., DataTemplate or ControlTemplate has always it's own namescope, as it describes a dynamic visual subtree.
Think about it: a DataTemplate, that is defined as the item template of a ItemsControl and contains an element named "TemplateElement" is applied to every item of this ItemsControl.
If the template wouldn't define its own isolated namescope, then there would be multiple elements of the same name - in the same namescope. Pretending that this is legal, how can the binding expression "know" which of the duplicate elements the binding uses as its source? They are duplicates from the naming point of view but unique regarding their content. This should make it clear that it can never work.
The DataTemplate is not the item. It's the construction blueprint for multiple items. Because of this multiplicity, it is not possible to bind from outside of the template to an element of this template. Same applies to the ControlTemplate.
Furthermore and most important, templates are part of the visual tree but not the logical tree. Beside the namescope in case of Binding.ElementName, this is the main reason why a Binding defined outside a template cannot resolve the source, if the source itself is part of a template. Binding.RelativeSource expressions only traverse the logical tree. This means a Binding can never find a composed element's internals (visual tree), but only the direct top-level elements (logical tree).

Binding the content of a ListBoxItem to something not related to

I'm new to WPF, but am pretty familiar with binding list box controls to observable collections in the view model.
In my current project we have a ListBox that is used for navigating to different pages in a frame box. I want to add some display information to the first ListBoxItem to show which object (in this case, the Scenario) is being worked on (it is selected in a previous frame that visible in the subsequent frames). The ListBox itself is using a static list defined in the xaml, so it isn't bound to anything in the ViewModel. The CurrentScenario is a property on the ViewModel. I was able to add a Label to the same window that contains this ListBox and successfully bind CurrentScenario.Id to its content, and it updated correctly, so I know that the path in the Binding statement should resolve correctly.
<ListBox
Style="{StaticResource FunctionBackground}"
IsSynchronizedWithCurrentItem="True"
>
<ListBoxItem Style="{StaticResource FunctionListBoxItemStyle}">
<ListBoxItem.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Scenario" />
<TextBlock Grid.Row="1" Text="{Binding Path=CurrentScenario.Id}"/>
</Grid>
</DataTemplate>
</ListBoxItem.ContentTemplate>
</ListBoxItem>
<ListBoxItem Style="{StaticResource FunctionListBoxItemStyle}" >Parameter</ListBoxItem>
<ListBoxItem Style="{StaticResource FunctionListBoxItemStyle}" >Run</ListBoxItem>
<ListBoxItem Style="{StaticResource FunctionListBoxItemStyle}" >Results</ListBoxItem>
</ListBox>
When I try to add this extra information to the listbox item, from what I can tell, the list box item has an empty text block below the text block with the word "Scenario." I can't figure out why the empty text box content is not showing the value of the bound property. When I put a normal string in the Text property of the second text block, it shows up correctly.
I imagine that either ListBoxItem content is only set up be bound to properties related to the ItemSource, and so it ignores attempts to bind to other things, or maybe there is something fundamental in WPF that I am missing. Or both...
Thanks if anyone has any ideas!
So if property CurrentScenario is in ViewModel you can use RelativeSource to binding to this property.
...
<TextBlock Grid.Row="0" Text="Scenario" />
<TextBlock Grid.Row="1" Text="{Binding Path=DataContext.CurrentScenario.Id, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
...

How to change from binding ObervableCollection to a TabControl to a ContentControl

I'm very new to WPF and am writing an application using this example as a starting point
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx#id0090025
I will only have one workspace visible at any one time, so I want to get rid of the TabControl and use something simple instead - probably a ContentControl, I'm really not sure but all it needs to do is have content in and be closable. So I am trying to replace this:
<DataTemplate x:Key="WorkspacesTemplate"><TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource ClosableTabItemTemplate}"
Margin="4"
/>
with:
<DataTemplate x:Key="WorkspacesTemplate">
<ContentControl Content="{Binding ??}" ContentTemplate="{StaticResource ClosableTabItemTemplate}"/>
</DataTemplate>
but I don't know what to bind to. The code in the example seems to use CollectionViewSource to set the active workspace - it's the active workspace that I am interested in but I don't understand what TabControl is doing except that it's something to do with IsSynchronizedWithCurrentItem="True"
The template is invoked from here (Workspaces is the ObservableCollection of ViewModels):
<HeaderedContentControl Content="{Binding Path=Workspaces}" ContentTemplate="{StaticResource WorkspacesTemplate}" Header="Workspaces" Style="{StaticResource MainHCCStyle}"/>
and here is the ClosableItem template:
<DataTemplate x:Key="ClosableTabItemTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=CloseCommand}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
Focusable="False"
FontFamily="Courier"
FontSize="9"
FontWeight="Bold"
Margin="0,1,0,0"
Padding="0"
VerticalContentAlignment="Bottom"
Width="16" Height="16"
/>
<ContentPresenter
Content="{Binding Path=DisplayName}"
VerticalAlignment="Center"
/>
</DockPanel>
</DataTemplate>
Please can someone explain what I need to do? Thanks
The WorkspacesTemplate is telling WPF how to display the Workspaces property, which, as you say is an ObservableCollection of ViewModels.
So, the WorkspacesTemplate says, display all these ViewModels in a tab control, and for each ViewModel, use the ClosableTabItemTemplate to display the ViewModel in a tab.
Since you only want one workspace visible at a time, you don't need to expose a collection of workspaces from your ViewModel, and you don't need a tab control to display them. You simply expose the one current workspace from your ViewModel and provide some XAML to display it.
If you still want to use a template to wrap the ViewModel, then yes, you can just use a ContentControl to invoke the template:
<DataTemplate x:Key="MySingleWorkspaceTemplate">
<TextBlock Text={Binding Blah} />
<!-- etc -->
</DataTemplate>
and to invoke the template
<ContentControl Content="{Binding CurrentWorkspace}" ContentTemplate="{StaticResource MySingleWorkspaceTemplate}"/>
However, if this is the only place that the XAML is going to be used, you might as well forget the template and just declare the XAML directly. Eg, (instead of ContentControl)
<TextBlock Text={Binding CurrentWorkspace.Blah} />
<!-- etc -->
EDITED TO ADD:
I think you might be getting confused because currently the ViewModel has no concept of the "Selected Workspace", it just exposes a collection. For completeness (but don't worry about all this), the selection is introduced by the TabControl which indirectly uses the default CollectionView for the Workspaces collection, and CollectionView has the concept of a selected item. This is all in the view.
I wouldn't worry about any of this now, just expose the one workspace yourself from your ViewModel.
EDIT2:
Your close button is appearing because you are explicitly setting a ContentTemplate on your HeaderedContentControl. This template will appear regardless of Content.
To make a template only appear when there is data in Content, make the template implicit instead. If you add a DataType to your template definition (and remove the key), you tell WPF to always use this template to display an object of that data type.
<DataTemplate DataType="{x:Type vm:WorkspaceViewModel}">
<!-- Blah -->
</DataTemplate>
Then you can remove the explicit template from your HeaderedContentControl. Simply setting the Content will be enough to invoke the template, and if there is no Content, there is no template.
<HeaderedContentControl Content="{Binding Path=CurrentWorkspace}" />
(ps. If you're not using the header of HeaderedContentControl, you might as well just use a bog standard ContentControl)

WPF - Databind to a StackPanel using DataTemplates

I've modified my question since it has changed focus when trying things out.
I narrowed the problem down to the following...
I try to bind the selected Item of a TreeView to a StackPanel (or some other container that can hold User Controls). This container will then display a UserControl, depending on the type of the selected item.
Here is the xaml of the StackPanel (both treeview and stackpanel are in the same window ==> different grid column)
<StackPanel Grid.Column="2" MinWidth="500" DataContext="{Binding ElementName=myTree, Path=SelectedItem, Mode=OneWay}">
<StackPanel.Resources>
<DataTemplate DataType="{x:Type mvTypes:MyTypeA}">
<controls:UserControlA DataContext="{Binding}" />
</DataTemplate>
<DataTemplate DataType="{x:Type mvTypes:MyTypeB}">
<controls:UserControlB DataContext="{Binding}" />
</DataTemplate>
</StackPanel.Resources>
</StackPanel>
When I place a user control directly under the stackpanel (not in the resources), it displays it with the selected object as their datacontext.
Idem if I place a TextBox in it, it will show the correct type of the selected item.
<TextBox Name="textBox1" Text="{Binding}" />
For some reason, placing it within a DataTemplate (even without setting the DataType) results in nothing to display.
Any sugestions. I'm thinking that maybe a StackPanel is not the right control for this, though I can't seem to find other controls that look suitable as containers like this.
Thanks in advance.
Replace the StackPanel in your example with ContentPresenter and instead of DataContext set the Content property. That should work.
Although you have set the Binding on the second custom control, are you setting the DataContext, as the binding is the route to the information and the DataContext is the information it applies this binding information to.
Andrew
You can create a UserControl to display the TreeView and the selection info on the right, all in one. It saves you from creating any custom control. A custom control is basically unnecessary since you do not create anything which didn't exist before.
<UserControl x:Class="NameSpace.SelectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="namespace.Controls"
Height="300" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TreeView Name="customTree">
<!--Items go here-->
</TreeView>
<StackPanel Grid.Column="1" MinWidth="50" DataContext="{Binding ElementName=customTree, Path=SelectedItem, Mode=OneWay}">
<StackPanel.Resources>
<DataTemplate DataType="{x:Type StylingTest:CustomViewModelA}">
<controls:CustomADetailsControl />
</DataTemplate>
<DataTemplate DataType="{x:Type StylingTest:CustomViewModelB}">
<controls:CustomBDetailsControl />
</DataTemplate>
</StackPanel.Resources>
<TextBlock Text="{Binding}"/>
</StackPanel>
</Grid>
</UserControl>
Any other custom behaviour, I'm sure you could create or set in styles/templates here.
Also, you might find one of my other answers useful.
Good luck with wpf, cheers.

What Does this MSDN Sample Code Do? - ItemsControl.ItemTemplate

This is a XAML code sample taken from the MSDN library article for the ItemsControl.ItemTemplate property:
<ListBox Width="400" Margin="10" ItemsSource="{Binding Source={StaticResource myTodoList}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=TaskName}" />
<TextBlock Text="{Binding Path=Description}"/>
<TextBlock Text="{Binding Path=Priority}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I'm looking for an explanation of the usage of the <StackPanel> element is this example.
->
Where is this panel going to exist in the ListBox?
What is its purpose in the ItemTemplate?
Can any System.Windows.Controls.Panel be used in its place, specifically a Grid?
How would I go about using a <Grid> element as the template for each item in the ListBox?
Here is the concept I am going for:
http://img151.imageshack.us/img151/7960/graphconcept.png
I have drawn the graph using a <Path> element, and there are no problems there.
I am working on the labels for the axies, and I am experimenting with the use of a <Grid> element in the ItemTemplate - but I have no idea how the grid is supposed to function in this context, and MSDN says nothing about the panel in their sample code.
My XAML for the Y-axis labels currently looks like this:
<ListBox Background="Transparent" BorderThickness="0" ItemsSource="{Binding Path=GraphLabelYData}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding Path=GraphLabelSpacing}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{Binding ElementName=GraphLabelYData, Path=GraphLabelMarkerLength}" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Bottom" Text="{Binding Path=GraphLabelTag}" />
<Rectangle Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Stroke="Black" Fill="Black" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Does this look correct? Nothing shows up at run-time, but I want to make sure the XAML is modeled correctly before I start debugging the data-bindings and the code-behind.
"Where is this panel going to exist in the ListBox?" - The listbox will make one copy of it for each list item, i.e. one for each element in the myTodoList collection. So within each list item, you'll have the three labels stacked one above the other.
"What is its purpose in the ItemTemplate?" - To make it possible to show more than one control for each element in the ItemsSource. ItemTemplate, like many things in WPF, can only take one child element, so if you want multiple children, you need to specify how you want them laid out, and you do that by adding a panel (StackPanel in this case).
"Can any System.Windows.Controls.Panel be used in its place, specifically a Grid?" - You bet.
"How would I go about using a <Grid> element as the template for each item in the ListBox?" - The same way you would use a Grid anywhere else. It's no different; it's just that ItemsControl (and its descendant, ListBox) will create multiple instances of your Grid. Note, though, that inside the ItemTemplate, your DataContext will be the current list item, and therefore your {Binding}s will be relative to that list item (unless you specify otherwise with e.g. ElementName).
"Does this look correct?" - This really should be posted as a separate question, as it's unrelated to the questions about the MSDN sample, and I'm not even sure what you're trying to do. But I'll try to answer: I suspect something is wrong, because you're using the name "GraphLabelYData" two different ways. In the ColumnDefinition, as far as I can tell, you're treating GraphLabelYData as the name of a XAML element (i.e. you're looking for another control in the window/page/UserControl with Name="GraphLabelYData" or x:Name="GraphLabelYData", and reading that control's GraphLabelMarkerLength property); but in the TextBlock, you're treating GraphLabelYData as the name of a property on the current collection item. I suspect one of those isn't right.

Resources