I have a ListBox that presents a databound list of objects via its ItemSource. Because each object has special display needs I’m defining an ItemTemplateSelector that returns the appropriate DataTemplate depending on the object. That all works without a hitch.
The DataTemplates for each object follow a common formula, but contains custom elements in the middle. For example:
<DataTemplate x:Key="collectibleTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border BorderBrush="LightGray" BorderThickness="1">
<Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
<StackPanel>
<TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />
<!-- This is the only custom part of each template -->
<StackPanel Margin="0,10,5,0" Orientation="Horizontal">
<Label Content="Type:" />
<ComboBox Height="22" HorizontalAlignment="Left" SelectedItem="{Binding Path=CollectibleType, Mode=TwoWay}"
ItemsSource="{Binding Source={StaticResource collectibleTypeFromEnum}}" />
</StackPanel>
<!-- End custom part -->
<StackPanel Margin="0,0,0,5">
<Label Content="Available Actions:" >
<Label.Style>
<Style TargetType="Label">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding EditActions.Count}" Value="0">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<ItemsControl ItemsSource="{Binding EditActions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding}" Content="{Binding Title}" ToolTip="{Binding ToolTip}" Margin="5,0,5,0"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</Expander>
</Border>
</Grid>
</DataTemplate>
As you can see there’s lots of shared XAML, wrapping a small custom section in the middle.
Additional data templates will be written by other engineers (they’ll want to create one for each new object type that they add), so I’m interested in making the creation of a new DataTemplate as fool-proof and painless as possible. No copying of the entire DataTemplate with the custom “stuff” added in the middle, of course – but I’m also not partial to extracting parts of the template as reusable parts and referencing them in because it still leads to lots of duplicate code in each new DataTemplate, and that means possible errors and hard maintainability. I.e., this right here is a more maintainable approach but still feels suboptimal:
<DataTemplate x:Key="collectibleTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border BorderBrush="LightGray" BorderThickness="1">
<Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
<StackPanel>
<TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />
<!-- This is the only custom part of each template -->
[...]
<!-- End custom part -->
<ContentPresenter Content="{StaticResource AvailableActions}" />
</StackPanel>
</Expander>
</Border>
</Grid>
</DataTemplate>
<StackPanel Margin="0,0,0,5" x:Key="AvailableActions" x:Shared="false">
<Label Content="Available Actions:" >
<Label.Style>
<!--
[Bottom half of shared XAML from the first example, offloaded here]
-->
</StackPanel>
So: what is my best strategy to solve this? AFAIK I’m stuck with using DataTemplates because that’s the only element that a ListBox ItemTemplateSelector accepts. Is there a way to create a compound DataTemplate in the DataTemplateSelector? I'd provide the stock DataTemplate that is shared by all objects, and the DataTemplateSelector references in the bit of custom XAML needed for each object type. Other engineers would hook into that generalized code behavior.
Not sure, fumbling a bit in the dark here as whether there is a pattern that allows me to solve this elegantly.
And, just for reference: my current DataTemplateSelector is super straightforward. This is where I would expect to construct the final DataTemplate, rather than simply returning one that's hardcoded in XAML.
public class NodeComponentDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null)
{
if (item is CollectibleComponent)
return element.FindResource("collectibleTemplate") as DataTemplate;
// [...]
}
}
}
You could create the DataTemplate dynamically using the XamlReader.Parse or XamlReader.Load method, e.g.:
string template = "<DataTemplate xmlns =\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x =\"http://schemas.microsoft.com/winfx/2006/xaml\"><StackPanel>[PLACEHOLDER]</StackPanel></DataTemplate>".Replace("[PLACEHOLDER]", "...custom code...");
return System.Windows.Markup.XamlReader.Parse(template) as DataTemplate;
The custom parts could be defined as UserControls.
I am afraid there is no way to base a DataTemplate on another one in pure XAML though.
You could create a new CustomControl that fits your needs. It will apply the style by itself and you can give additional DepdendencyProperties to make it more convinient. In the end you can still put it in a DataTemplate to use it with your DataTemplateSelector.
Related
I've a problem to connect.
I started to connect my tabs with a tabcontrol.ressources and it worked to show the text of each tabs.
Then I wanted to had a scroll for my TabItems and it doesn't work, nothing shows in tab... I can't even use tabcontrol.ressources anymore...
<DockPanel>
<Button Background="DarkGoldenrod" Height="Auto" Command="{Binding OpenFlyoutDataCommand}">
<StackPanel>
<materialDesign:PackIcon Kind="ArrowRightBoldCircleOutline" Width="30" Height="30"/>
</StackPanel>
</Button>
<TabControl ItemsSource="{Binding TabEDCWaferData, Mode=TwoWay}"
SelectedItem="{Binding SelectedTabEDCWaferData}">
<!-- Used to create a scroolbar for tabitems -->
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden" >
<TabPanel Grid.Column="0" Grid.Row="0"
Margin="2,2,2,0" IsItemsHost="true"/>
</ScrollViewer>
<ContentPresenter ContentSource="..."/>
</Grid>
</ControlTemplate>
</TabControl.Template>
<!-- Contains the text in the tab item ! -->
<TabControl.Resources>
<DataTemplate DataType="TabItem">
<DockPanel>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Content}" />
</DockPanel>
</DataTemplate>
</TabControl.Resources>
</TabControl>
</DockPanel>
This is connected to a collection of TabItem, where I've a function to add Items binding to an other button.
private ObservableCollection<TabItem> _TabEDCWaferData;
public ObservableCollection<TabItem> TabEDCWaferData
{
get { return _TabEDCWaferData; }
set
{
_TabEDCWaferData = value;
RaisePropertyChanged("TabEDCWaferData");
}
}
public void AddTabItem(string name)
{
TabItem tab = new TabItem();
tab.Header = name;
tab.Content = "Temporary content";
TabEDCWaferData.Add(tab);
}
I read that I have to use the ContentPresenter, but I don't know how to bind it. I think this is not working with TabItems...
I just want to bind it as I did in the Ressources by using the ContentPresenter.
I hope that I'm clear enough ! Thanks
EDIT : I try to display in the ContentPresenter the selected item tab content that I add in the function `AddTabItem.
With ContentPresenter, most times, this does the job:
<ContentPresenter />
The default ContentSource is "Content". That means it'll look at the Content property of the templated parent and it'll take whatever it finds there for its own content.
But that doesn't help you at all, and you don't have to use ContentPresenter; it's just a convenience. In this case, the content you want to present is SelectedItem.Content, which isn't a valid ContentSource for ContentPresenter. But you can do the same thing with a binding on a ContentControl instead:
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ScrollViewer
Grid.Row="0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Hidden"
>
<TabPanel
Grid.Column="0"
Margin="2,2,2,0" IsItemsHost="true"/>
</ScrollViewer>
<ContentControl
Grid.Row="1"
Content="{Binding SelectedItem.Content, RelativeSource={RelativeSource TemplatedParent}}"
/>
</Grid>
</ControlTemplate>
</TabControl.Template>
TemplateBinding isn't going to work with a Path such as "SelectedItem.Content"; it only accepts names of properties on the templated parent. I fixed your Grid.Row attributes, too.
Also, you may as well delete that DataTemplate for TabItem that you put in TabControl.Resources. That's not what DataTemplate is for; you use DataTemplates to define visual presentations for your viewmodel classes, but TabItem is a control. It already knows how to display itself, and in fact that DataTemplate is being ignored, so it's best not to leave it there; you'll only waste time later on making changes to it and trying to figure out why it's not having any effect. Your TabItems will display correctly without it.
Try something like this ?
<ContentPresenter Content="{TemplateBinding Content}" />
Edit
<ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" />
I'm a bit beginner in WPF, so I ask this..
Let's say I have a window, and inside the window I want to have something like container, could be just border or maybe panel (in winform terms). The content of container is binded to the selected option (e.g:button). So, for instance, when user selects OPTION 1, the container shows chart; when user selects OPTION 2, the container shows listview filled with data; when user selects OPTION 3, the container shows another things, and so on.
What is the best/nicest (or easiest maybe) approach to do this? I'm thinking about using user control for the content of the container, but don't know if this is nice solution neither the performance for using user control to show little bit complex things and maybe some calculations. Any other idea guys?
To elaborate on #Sheridan's answer, here is a simple TabControl XAML that does what you need:
<TabControl TabStripPlacement="Left">
<TabItem Header="Option 1">
<Grid Background="DarkGray">
<TextBlock Foreground="AliceBlue" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Text="View 1"/>
</Grid>
</TabItem>
<TabItem Header="Option 2">
<Grid Background="DarkBlue">
<TextBlock Foreground="AliceBlue" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Text="View 2"/>
</Grid>
</TabItem>
<TabItem Header="Option 3">
<Grid Background="DarkSlateBlue">
<TextBlock Foreground="AliceBlue" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Text="View 3"/>
</Grid>
</TabItem>
</TabControl>
Result:
You can customize it a little bit by adding this simple Style To your Window.Resources:
<Window.Resources>
<Style TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<RadioButton Content="{TemplateBinding Header}" Margin="2"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
Which then results in:
The "WPF Mentality" makes you think the UI controls in terms of their functionality, not their appearance, this is a TabControl =)
I solved this with a ContentControl
MainWindow:
(Define the views you wish to visualize as resources)
<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:SystemPCViewModel}">
<controls:SystemPCControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ChipPCViewModel}">
<controls:ChipPCControl/>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding CurrentView}"/>
</Grid>
ViewModel: (can't get much simpler)
public ViewModelBase CurrentView
{
get { return currentView; }
set { Set(() => CurrentView, ref currentView, value); }
}
And there you go, you can change your views by setting the view model for the controls you defined in your MainWindow
private void OnCommandExecuted()
{
CurrentView = someViewModel;
}
private void OnAnotherCommandExecuted()
{
CurrentView = anotherViewModel;
}
HTH!
What you are describing sounds pretty close to a standard TabControl, but with a ControlTemplate that puts the tabs on the left side instead of above the content panel. Using this method would mean having a UserControl in each TabItem, eg. multiple controls. You can find out more about the TabControl from the TabControl Class page at MSDN.
I have the grid with wpf map control and canvas over it for drawing markers over map.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<UserControl Grid.Row="0" Content="{Binding CurrentMap}" />
<UserControl Grid.Row="0" Content="{Binding DrawningCanvas}" />
</Grid>
Now I needed to use more than 1 layer over the map.
What wpf control I should use for binding to ObservableCollection<Canvas> in DataContext and draw them all over each other?
You seem to be somewhat confused. An ObservableCollection is a collection for data items. A Canvas is a UI control. Therefore, you should never have a data item collection with UI elements in.
Instead, you could have a Marker data class that contains X and Y properties for the marker's positions and maybe Name and ImageSource properties to define what it would look like in the UI. Then you could have a ObservableCollection of type Marker named Markers that you could bind to the ItemsSource property of an ItemsControl. Finally, you could set a Canvas as the ItemsPanel of the ItemsControl:
<ItemsControl ItemsSource="{Binding Markers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type YourXmlNamespacePrefix:Marker}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageSource}" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I don't really know why you would want to have multiple layers because you could overlay multiple items on the same layer, but if you really need multiple layers, then you can just add more of the ItemsControl elements shown above.
I have got a ListView wich I added Elements by binding. The ListView looks like:
<ListView
x:Name="ListView"
Height="auto"
Width="350"
ItemsSource="{Binding}"
Padding="0,0,-20,0"
Grid.Row="1"
Grid.Column="0"
Background="#EFEFEF"
ItemContainerStyle="{StaticResource ListViewStyle}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Height="50" VerticalAlignment="Top" Margin="0,0,0,0"
<TextBlock Text="{Binding name} TextWrapping="NoWrap"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
With this basic setup there is already an animation when a element is bonded to the underlying List. Strangely to different animations are used. The first element slides in from the right and all other elements popup. I’m searching for a way to animate all added elements the same way (e.g. slide in from the right). I have been locking into the auto generated (by Blend) ListViewStyle for hours now but couldn't find something. Later I found out that it is possible to add this property inside the style:
<Style x:Key="ListViewStyle" TargetType="ListViewItem">
<Setter Property="Transitions">
<Setter.Value>
<TransitionCollection>
<EntranceThemeTransition FromHorizontalOffset="400" />
<PopupThemeTransition FromHorizontalOffset="400"/>
</TransitionCollection>
</Setter.Value>
</Setter>
...
</Style>
The EntranceThemeTransition and PopupThemeTransition seems to be the right properties because they change the behavior of the animation. But I don't know how to use them or how to disable one. How can I get just one animation (slide in from the right) to the ListView?
This should work:
<ListView
x:Name="lv">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel>
<VirtualizingStackPanel.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition
FromHorizontalOffset="400" />
</TransitionCollection>
</VirtualizingStackPanel.ChildrenTransitions>
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
*Update:
You can also use ListView.ItemContainerTransitions to define these transitions.
In my WPF application I have a ComboBox control that is located inside a Grid Control. In XAML I am assigning a name to the ComboBox:
<DataGridTemplateColumn Header="Status">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock VerticalAlignment="Center" Text="{Binding name_ru}" Width="Auto" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Name="stcom" Style="{DynamicResource ComboBoxStyle}" SelectionChanged="status_SelectionChanged" Height="auto" Width="Auto">
<ComboBox.BorderBrush>
<SolidColorBrush Color="{DynamicResource Color1}"/>
</ComboBox.BorderBrush>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
With the method FindName(string) I am trying to refer to the ComboBox with its associated name:
ComboBox stcom
{
get
{
return (ComboBox)FindName("stcom");
}
}
if (stcom != null)
{
stcom.ItemsSource = list;
}
But obviously the control can not be found because the reference stcom remains null.
The question now is how to refer to my ComboBox using its name property ?
The answer is:
<Style x:Key="CheckBoxStyle1" TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<StackPanel Orientation="Horizontal">
<Grid>
<TextBlock Name="tbUserIcon" Text="t1" />
<TextBlock Name="tbCheck" Text="✓" />
</Grid>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and C#:
checkBox.ApplyTemplate();
var tbUserIcon= (TextBlock)checkBox.Template.FindName("tbUserIcon", checkBox);
don't forget the checkBox.ApplyTemplate() be fore Template.FindName() it's important!
First you have to get access to the control template which it has been applied to, then you can find an element of the template by name.
Have a look at the MSDN knowledge base :
How to: Find ControlTemplate-Generated Elements
You can't access controls that are part of a DataTemplate with their name.
You can try to read about some workarounds for example
WPF - Find a Control from DataTemplate in WPF
You can also have a look at the dozens of posts here on SO issuing this topic for example
here
here
here
here
here
here
here
here