ItemsControl with only custom subelements - wpf

I would like to build a custom component that layouts its childs in either a StackPanel or a Grid (with variable row count, which makes me consider the StackPanel instead). The items are custom elements/objects that just hold some configuration, based on which a few controls are created to display them (some labels and text boxes).
Ideally, the component should be used somehow like this (where SpecializedCustomPanelItem is a subtype of CustomPanelItem):
<CustomPanel>
<CustomPanelItem Param1="value A" Param2="value B">Text</CustomPanelItem>
<CustomPanelItem Param1="value C">Other text</CustomPanelItem>
<SpecializedCustomPanelItem>More text</SpecializedCustomPanelItem>
<!-- The number of items is variable -->
</CustomPanel>
I’ve read on the ItemsControl for a while now, and it fits my needs rather well. I would create simply types for the items, and make data templates for them available from inside the ItemsControl. Then they should already render fine.
However I would like to require the items inside that ItemsControl to be of a specific type (i.e. CustomPanelItem or a subtype). I actually thought that the ItemsControl would allow this, just like you within a ComboBox or a MenuItem, but it turns out that it actually allows any subtype, and if necessary wraps them in a item container.
So I have been thinking if an ItemsControl is actually what I am looking for, as I do not want any “fancy” things like selection or scrolling which most of those controls implement. I actually only want to build a simple interface to a common pattern in the application that auto generates those components and layouts them in a Grid/StackPanel.
Should I still be using the ItemsControl or rather build some more custom component?

In this case you don't really need a custom component. Changing the ItemsPanel type to whatever type you need + multiple templates for the Items should do the trick.
However to answer the question in the heading: If you want to force an items control to only accept a certain type of items, you will have to create
a. A CustomItemsControl
b. A CustomItemsControlItem
Then for the CustomItemsControl you should declare the attribute
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CustomItemsControlItem))]
Then you also will need to
protected override DependencyObject GetContainerForItemOverride()
{
return new CustomItemsControlItem();
// You can throw an exception here
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is CustomItemsControlItem;
}
If memory serves this should force the ItemsControl to not allow other types to be added as children and should throw exceptions. You could then do some magic inside CustomItemsControlItem by defining some DependencyProperties which you can then set when adding the items in XAML.
But yet if you have multiple types in your ViewModel that you want to display correctly, the correct way is still to provide multiple templates for the CustomItemsControlItem targetting your ViewModel types.
Hope this helps.

This sounds perfect for an ItemsControl
You can set it's ItemsPanelTemplate to define the kind of panel which will hold your items, and set the ItemContainerTemplate to define how to draw each item.
If items should be drawn differently based on what type they are, I'd suggest using implicit DataTemplates instead of setting the ItemContainerTemplate
<Window.Resources>
<DataTemplate DataType="{x:Type my:BasePanelItem}">
<my:CustomPanelItem Param1="{Binding Param1}" Param2="{Binding Param2}" Content="{Binding SomeValue}" />
</DataTemplate>
<DataTemplate DataType="{x:Type my:SpecializedPanelItem}">
<my:SpecializedCustomPanelItem Content="{Binding SomeValue}" />
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding MyItems}">
<!-- ItemsPanelTemplate -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<my:CustomPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
You mentioned that you wanted to perhaps use a dynamically created Grid instead of a StackPanel as well. If you do, you might be interested in some GridHelpers I have posted on my blog. This would allow you to bind the number of Columns/Rows on the Grid in the ItemsPanelTemplate
<ItemsControl ItemsSource="{Binding MyCollection}">
<!-- ItemsPanelTemplate -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid local:GridHelpers.RowCount="{Binding RowCount}"
local:GridHelpers.ColumnCount="{Binding ColumnCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- ItemContainerStyle -->
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding ColumnIndex}" />
<Setter Property="Grid.Row" Value="{Binding RowIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

Related

Create dummy data type so I can have a DataProvider to my ItemsControl in xaml

I am developing an UserControl in XAML consisting of an image of the human body, where some electrode position points are to be specified.
Each position would be modeled by some datatype containing properties for Position, Orientation (angle) and State (enabled, disabled, activated):
I want to use sample data (DataProvider or something similar) defined directly in XAML so I can design position and orientation visually and try some different possibilities by editing the single XAML file and having immediate visual feedback.
So I will create an ItemsControl with a Canvas, define a DataTemplate for my objects, and then I would like to write something similar to this XAML:
<ItemsControl x:Name="DotsOverBody"
ItemsSource="{StaticResource SomeLocalXamlDefinedDataSource}"
ItemTemplate="{StaticResource SomeSuitableTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- Could I define a generic (xml-based?) "Body Position" type
directly in XAML? -->
<BodyPosition X="123" Y="456" Angle="45"/>
<BodyPosition X="133" Y="476" Angle="33"/>
<BodyPosition X="125" Y="356" Angle="77"/>
<BodyPosition X="323" Y="457" Angle="89"/>
<BodyPosition X="528" Y="426" Angle="10"/>
<BodyPosition X="163" Y="156" Angle="3"/>
</ItemsControl>
A screenshot of a legacy application we have is below, with my intended visual output (I already have a DataTemplate for that, which I would change the target type of course).
So how could I achieve this?

how to prevent using global DataTemplate without specifiying new one

<DataTemplate DataType="{x:Type MyType}">
...
</DataTemplate>
I have a default DataTemplate for MyType.
want to prevent using it below without having to specify a real DataTemplate
<ItemsControl ItemsSource="{whateverList of MyType}" ItemTemplate="{x:Null}"/>
ItemTemplate="{x:Null}" doesn't get the job done -> shows default DataTemplate
would be happy with "ToString()" display
any ideas?
If you want to override the default data template for the type, you will have to specify a different one:
<ItemsControl ItemsSource="{listOfMyType}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- Whatever -->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
There is some more discussion about this approach here, including another's attempt at using {x:Null} for the template.
If your template needs to bind to the ToString() for your type (and no property exists in the type to do this for you), you'll need to use an IValueConverter, as discussed here.

Place a ContentControl inside a custom control XAML

I created a custom control and what to use a ContentControl inside that control for use of the MVVM design pattern however my control does not like this when I run the application. For testing I also tried other standard controls but none of them work inside the custom control... just more custom controls that are dependant on the parent custom control.
Does anybody suggest how to place standard controls such as the ContentControl inside a custom control?
Cheers.
EDIT
XamlParseException -> 'Add value to collection of type
'System.Collections.ObjectModel.ObservableCollection(Ribbon_Framework.RibbonTabItem)'
threw an exception.' Line number '8' and line position '14'.
<Ribbon:Ribbon AutomaticStateManagement="True" x:Name="Ribbon">
<ContentControl x:Name="SearchRibbon" Content="{Binding Path=SearchRibbon}" ContentTemplate="{DynamicResource SearchRibbonTemplate}" />
</Ribbon:Ribbon>
Inside the contentcontrol ->
<DataTemplate x:Key="SearchRibbonTemplate">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ribbon:RibbonTabItem Header="Search">
<Ribbon:RibbonGroupBox Header="{Binding Path=DisplayName}" Width="100">
<Ribbon:Button Width="100" Icon="{Binding Path=TemplateResource}" LargeIcon="{Binding Path=TemplateResource}" Command="{Binding Path=Commands}" />
</Ribbon:RibbonGroupBox>
</Ribbon:RibbonTabItem>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
Your Ribbon control expects an object of RibbonTabItem type (since it houses an
ObservableCollection<RibbonTabItem>
so you can only add the RibbonTabItem class to it - you need to make sure your control allows other elements inside it. Some 3rd party controls get round this by providing a content control inside the inner item of their custom control (i.e. let RibbonTabItem have a ContentControl inside it) or allow you to customise the item template
You need to change your implementation of Ribbon or change the functionality of RibbonTabItem for this to work. Look at ItemsControl.Items property and see what type that is. You should try using that type for your ObservableCollection

Binding from resources inside a DataTemplate

Is there some way to get the DataContext of a DataTemplate to use in bindings within its resources?
<DataTemplate x:Key="History">
<ItemsControl ItemsSource="{Binding History}">
<ItemsControl.Resources>
<app:BitmapProvider x:Key="Converter" ShowDetails="True"
Type="{Binding Model.Type}" />
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Data, Converter={StaticResource Converter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
The above template is used as the CellTemplate of a ListBox. The object at that level has two properties, History (containing a list of "historic info" objects) and Model (containing a bunch of other stuff, including Type). I'm using an ItemsControl to display the historic items next to each other; I want to display an image for each one, and the image is obtained from the BitmapProvider, which is an IValueConverter.
The converter needs two bits of info to obtain a result: one is the Data of the individual historic items, and the other is the Type of the whole collection. An added complication is that constructing this particular converter (or changing the Type given to it) is expensive, so I don't want to put it at the level of the individual history item, or to use a MultiBinding, and I can't put it outside of the template because then it won't have access to the Type.
Unfortunately, the above gives me the following error:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=Model.Type; DataItem=null; target element is 'BitmapProvider' (HashCode=57142809); target property is 'Type' (type 'TypeDetails')
Which I understand to mean that the resource can't figure out how to get the DataContext of the element it's contained within.
(I have searched, and most of the answers I could find suggested moving it outside the template or using a MultiBinding instead -- neither of which would really work in this case, as far as I can tell, as I've explained above. But I'd be delighted to be proven wrong, or given another alternative.)
I think you can accomplish that with DataContextSpy.
try something like:
<ItemsControl.Resources>
<spy:DataContextSpy x:Key="Spy"/>
<app:BitmapProvider x:Key="Converter" ShowDetails="True"
Type="{Binding DataContext.Model.Type,Source={StaticResource Spy}}" />
</ItemsControl.Resources>

WPF Dynamic Binding X and Y Co-ordinates

I have a question on WPF dynamic positioning.
I want to place Elipses on the screen based on X and Y co-ordinates that i have stored in a collection in C#.
I have been made aware of the drawing ability in WPF which you do from C# using the Windows.Media and Windows.Shapes.
Now what i actually want to do is use these namespaces to draw the elipses in the first case in a canvas all done in c# using my datasource that i have in c# to position the elipses using the x and y co-ordinates.
Now the complex part which is confusing me is what if the data in the datasource is changed as the data in the database changes, i will implement some sort of routine that checks the database every few seconds pulling back back any data that has changed since the last retrieval. Now i have seen the IPropertyChanged interface which i will inhert from for my class that i expose as my datasource for the page so when i retrieve the updated dataset i can call the PropertyChanged event which will notify WPF that the datasource has changed.
How would i bind the elipses in the UI when i was laying them out originally in C# to certain items from the datasource so when the datasource changed the elipses would automatically change as required to reflect the changed datasource as long as the ID for each x and y co-ordinate remained the same. So can i bind to specific rows from the collection for each elipse in my canvas when i'm setting them out?
I don't even know if its possible to bind a datasource to a Canvas inside which i can use the collection as i require to begin with but i thought i'd put this question out there incase someone has done something similar so i have a good starting point.
Thanks
Iffy.
To build on what others have said here is a complete self contained example - you can copy it straight into kaxaml or xamlpad (or blend, but I think in that case it has to go into a body of a usercontrol or a window) and see how it works.
Instead of using the rendertransform I prefer to use the canvas and set the left and top property, I just find it more readable that way. Alternatively, you can use a grid and set the margin but then you'll need a value converter of some kind.
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.Resources>
<!-- This is our list of shapes, in this case an inline XML list -->
<XmlDataProvider x:Key="ShapeList">
<x:XData>
<ObjectList xmlns="">
<Shapes>
<shape height="30" width="30" x="50" y="50"/>
<shape height="30" width="40" x="100" y="100"/>
<shape height="30" width="50" x="150" y="150"/>
<shape height="30" width="60" x="200" y="200"/>
<shape height="30" width="70" x="250" y="350"/>
</Shapes>
</ObjectList>
</x:XData>
</XmlDataProvider>
</Grid.Resources>
<ItemsControl ItemsSource="{Binding Source={StaticResource ShapeList}, XPath=ObjectList/Shapes/*}">
<!-- this template sets the panel as canvas for easy positioning -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- this template defines how each bound item is represented -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="{Binding XPath=#width}" Height="{Binding XPath=#height}">
<Ellipse Fill="White" Stroke="Black" StrokeThickness="2"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<!-- This style positions each bound item's container -->
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding XPath=#x}"/>
<Setter Property="Canvas.Top" Value="{Binding XPath=#y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
Instead of binding to an inline xml list you can bind to a collection on your viewmodel (best choice), a dependency property on your control or window, set the resource from codebehind, etc.
The key point is that you shouldn't be laying out the ellipses in C# unless you absolutely have to. Provide the data as some sort of a list of meaningful objects. Then create a data template that defines how that data is represented. Assuming you don't have to do any sort of complicated processing of your object to get the relevant ellipse properties you should be able to do this without any code, or at most with a few value converters.
This is the sort of UI separation that allows you to deal with updating the datasource (business logic) and displaying items (ui) separately.
So basically the idea is:
Expose a collection of objects - in my example this would be a collection of classes mirroring the structure of the shape xml element in the list. This can be the business object itself, or a viewmodel - a class that wraps a business objects and exposes conveniently bindable properties (in this case, position and size). The collection itself would prefereably be an ObservableCollection, so that UI is notified when you add or remove objects. Toss in some design time data into it if possible.
Bind to the collection, using the WPF datatemplates to define how an element should be presented. In this case I used a plain ItemsControl with a few simple templates, but this can be as complex as required
Work out how the collection will be updated from the original datasource. If you set up the previous steps correctly this is essentially a separate problem
You can use a translate transform to position the ellipses as you create them.
TranslateTransform transform = new TranslateTransform();
transform.X = X;
transform.Y = Y;
Ellipse ellipse = new Ellipse();
ellipse.RenderTransform = transform;
...
You could store the ellipses in a dictionary with the id as they key for quick and easy retrieval.
TranslateTransform transform = data[id].RenderTransform as TranslateTransform;
transform.X = newX;
transform.Y = newY;
You can accomplish this within a DataTemplate if your Ellipse objects are represented by a class, and perhaps displayed in an ItemsControl.
<Ellipse>
<Ellipse.LayoutTransform>
<TranslateTransform X="{Binding XCoord}"
Y="{Binding YCoord}" />
</Ellipse.LayoutTransform>
</Ellipse>
You would choose between LayoutTransform and RenderTransform based on the panel which held your Ellipse objects.
I also recommend reviewing an article by Bea Stollnitz (neé Costa) which shows how to leverage a ListBox backed by a Canvas with DataBinding to produce offset objects. Very cool.

Resources