How to build a grid of controls in XAML? - wpf

I am trying to build a UI in WPF to a specification. The UI is for editing a collection of items. Each item has an editable string property, and also a variable number of read-only strings which the UI needs to display. It might look something like this:
or, depending on data, might have a different number of text label columns:
The number of text columns is completely variable and can vary from one to "lots". The specification calls for the columns to be sized to fit the longest entry (they are invariably very short), and the whole thing should look like a grid. This grid will be contained in a window, stretching the text box horizontally to fit the window.
Importantly, the text boxes can contain multi-line text and will grow automatically to fit the text. The rows below need to be pushed out of the way if that happens.
Question: what would be a good way of doing this in WPF?
Coming from a WinForms background, I am thinking of a TableLayoutPanel, which gets populated directly by code I write. However, I need to do this in WPF. While I could still just get myself a Grid and populate it in code, I would really rather prefer a way that's more in line with how things are done in WPF: namely, define a ViewModel, populate it, and then describe the View entirely in XAML. However, I can't think of a way of describing such a view in XAML.
The closest I can get to this using MVVM and XAML is to use an ItemsControl with one item per row, and use a data template which, in turn, uses another ItemsControl (stacked horizontally this time) for the variable number of labels, followed by the text box. Unfortunately, this can't be made to align vertically in a grid pattern like the spec requires.

This does not map all too well, you could probably use a DataGrid and retemplate it to look like this. In other approaches you may need to imperatively add columns or the like to get the layout done right.
(You can hook into AutoGeneratingColumn to set the width of that one writeable column to *)

You can create your own Panel and then decide on how you want the layout logic to work for the children that are put inside it.
Look at this for inspiration:
http://www.nbdtech.com/Blog/archive/2010/07/27/easy-form-layout-in-wpf-part-1-ndash-introducing-formpanel.aspx
You could have a "ColumnCount" property, and then use that within the MeassureOverride and ArrangeOverride to decide when to wrap a child.
Or you could modify this bit of code (I know it's Silverlight code, but it should be close to the same in WPF).
http://blogs.planetsoftware.com.au/paul/archive/2010/04/30/autogrid-ndash-part-1.aspx
Instead of having the same width for all columns (the default is 1-star "*"), you could add a List/Collection property that records the different column widths sized you want, then in the AutoGrid_LayoutUpdated use those widths to make the ColumnDefinition values.

You've asked for quite a bit, the following code shows how to build a grid with the controls you want that sizes as needed, along with setting up the bindings:
public void BuildListTemplate(IEnumerable<Class1> myData, int numLabelCols)
{
var myGrid = new Grid();
for (int i = 0; i < myData.Count(); i++)
{
myGrid.RowDefinitions.Add(new RowDefinition() { Height= new GridLength(0, GridUnitType.Auto)});
}
for (int i = 0; i < numLabelCols; i++)
{
myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
}
myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
for (int i = 0; i < myData.Count(); i++)
{
for (int j = 0; j < numLabelCols; j++)
{
var tb = new TextBlock();
tb.SetBinding(TextBlock.TextProperty, new Binding("[" + i + "].labels[" + j + "]"));
tb.SetValue(Grid.RowProperty, i);
tb.SetValue(Grid.ColumnProperty, j);
tb.Margin = new Thickness(0, 0, 20, 0);
myGrid.Children.Add(tb);
}
var edit = new TextBox();
edit.SetBinding(TextBox.TextProperty, new Binding("[" + i + "].MyEditString"));
edit.SetValue(Grid.RowProperty, i);
edit.SetValue(Grid.ColumnProperty, numLabelCols);
edit.AcceptsReturn = true;
edit.TextWrapping = TextWrapping.Wrap;
edit.Margin = new Thickness(0, 0, 20, 6);
myGrid.Children.Add(edit);
}
contentPresenter1.Content = myGrid;
}
A Quick Explanation of the above All it is doing is creating the grid, defines rows for the grid; and a series of columns for the grid that auto size for the content.
Then it simply generates controls for each data point, sets the binding path, and assigns various other display attributes along with setting the correct row/column for the control.
Finally it puts the grid in a contentPresenter that has been defined in the window xaml in order to show it.
Now all you need do is create a class with the following properties and set the data context of the contentPresenter1 to a list of that object:
public class Class1
{
public string[] labels { get; set; }
public string MyEditString { get; set; }
}
just for completeness here is the window xaml and constructor to show hooking it all up:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<ContentPresenter Name="contentPresenter1"></ContentPresenter>
</Window>
public MainWindow()
{
InitializeComponent();
var data = new List<Class1>();
data.Add(new Class1() { labels = new string[] {"the first", "the second", "the third"}, MyEditString = "starting text"});
data.Add(new Class1() { labels = new string[] { "col a", "col b" }, MyEditString = "<Nothing>" });
BuildListTemplate(data, 3);
DataContext = data;
}
You can of course use other methods such as a listview and build a gridview for it (I'd do this if you have large numbers of rows), or some other such control, but given your specific layout requirements probably you are going to want this method with a grid.
EDIT: Just spotted that you're looking for a way of doing in xaml - tbh all I can say is that I don't think that with the features you're wanting that it is too viable. If you didn't need to keep things aligned to dynamically sized content on seperate rows it would be more viable... But I will also say, don't fear code behind, it has it's place when creating the ui.

Doing it in the code-behind is really not a WPFish(wpf way).
Here I offer you my solution, which looks nice imo.
0) Before starting, you need GridHelpers. Those make sure you can have dynamically changing rows/columns. You can find it with a little bit of google:
How can I dynamically add a RowDefinition to a Grid in an ItemsPanelTemplate?
Before actually implementing something, you need to restructure your program a little. You need new structure "CustomCollection", which will have:
RowCount - how many rows are there(implement using INotifyPropertyChanged)
ColumnCount - how many columns are there(implement using INotifyPropertyChanged)
ActualItems - Your own collection of "rows/items"(ObservableCollection)
1) Start by creating an ItemsControl that holds Grid. Make sure Grid RowDefinitions/ColumnDefinitions are dynamic. Apply ItemContainerStyle.
<ItemsControl
ItemsSource="{Binding Collection.ActualItems,
Converter={StaticResource presentationConverter}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid
local:GridHelpers.RowCount="{Binding Collection.RowCount}"
local:GridHelpers.StarColumns="{Binding Collection.ColumnCount,
Converter={StaticResource subtractOneConverter}"
local:GridHelpers.ColumnCount="{Binding Collection.ColumnCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type FrameworkElement}">
<Setter Property="Grid.Row" Value="{Binding RowIndex}"/>
<Setter Property="Grid.Column" Value="{Binding ColumnIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
The only thing left to do: implement presentationConverter which converts your Viewmodel presentation to View presentation. (Read: http://wpftutorial.net/ValueConverters.html)
The converter should give back a collection of items where each "label" or "textbox" is a seperate entity. Each entity should have RowIndex and ColumnIndex.
Here is entity class:
public class SingleEntity
{
..RowIndex property..
..ColumnIndex property..
..ContentProperty.. <-- This will either hold label string or TextBox binded property.
..ContentType..
}
Note that ContentType is an enum which you will bind against in ItemsTemplate to decide if you should create TextBox or Label.
This might seem like a quite lengthy solution, but it actually is nice for few reasons:
The ViewModel does not have any idea what is going on. This is purely View problem.
Everything is dynamic. As soon you add/or remove something in ViewModel(assuming everything is properly implemented), your ItemsControl will retrigger the Converter and bind again. If this is not the case, you can set ActualItems=null and then back.
If you have any questions, let me know.

Well, the simple yet not not very advanced way would be to fill the UI dynamically in the code-behind. This seems to be the easiest solution, and it more or less matches your winforms experience.
If you want to do it in a MVVM way, you should perhaps use ItemsControl, set the collection of items as its ItemsSource, and define a DataTemplate for your collection item type.
I would have the DataTemplate with something like that:
<Window x:Class="SharedSG.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:SharedSG"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type app:LabelVM}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="G1"/>
<ColumnDefinition SharedSizeGroup="G2"/>
<ColumnDefinition MinWidth="40" Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="{Binding L1}" Grid.Column="0"/>
<Label Content="{Binding L2}" Grid.Column="1"/>
<TextBox Grid.Column="2"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid Grid.IsSharedSizeScope="True">
<ItemsControl ItemsSource="{Binding}"/>
</Grid>
</Window>

You're probably way past this issue, but I had a similar issue recently and I got it to work surprisingly well in xaml, so I thought I'd share my solution.
The major downside is that you have to be willing to put an upper-bound on what "lots" of labels means. If lots can mean 100s, this won't work. If lots will definitely be less than the number of times you're willing to type Ctrl+V, you might be able to get this to work. You also have to be willing to put all the labels into a single ObservableCollection property in your view model. It sounded to me in your question that you already tried that out anyway though.
I takes advantage of AlternationIndex to get the index of the label and assign it to a column. Think I learned that from here. If an item has < x labels the extra columns won't get in the way. If an item has > x labels, the labels will start stacking on top of each other.
<!-- Increase AlternationCount and RowDefinitions if this template breaks -->
<ItemsControl ItemsSource="{Binding Labels}" IsTabStop="False" AlternationCount="5">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Grid.Column"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(ItemsControl.AlternationIndex)}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid IsItemsHost="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition SharedSizeGroup="B"/>
<ColumnDefinition SharedSizeGroup="C"/>
<ColumnDefinition SharedSizeGroup="D"/>
<ColumnDefinition SharedSizeGroup="E"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

Related

WPF "partial forms"

I'm making an application for DB migrations. I made a multithreaded framework with WPF GUI. I put someting like this in my namespace/folder:
class Something : Migrator {
public override Run(){
//I would need this
string valueOfMyCustomFieldOnForm = xyz.Text; //example
int count = 500;
for(int i = 0; i < 500; i++){
//do something here
OnProgressChanged(...); //call event, GUI is updated
}
OnCompleted(...); //migration completed
}
}
Then using reflection I put all classes in that namespace onto dropdown list. When I choose one in a list and click Start, the Thread with code in Run method is started.
DB Host: TEXTBOX
DB Username: TEXTBOX
DB Password: TEXTBOX
--
Migrator custom field 1: TEXTBOX
Migrator custom field 2: TEXTBOX
...
--
List with migrated items - irrelevant
There are few commong field on GUI (like database host, username etc...). But for some of those migrators I would need custom fields on GUI (for example 3 extra textbox fields).
What is the best way to do this in WPF? I need part of the GUI to be dynamic.
There's a lot of seemingly-irrelevant information in your question, which - I think - is really about mechanisms for creating metadata-driven UIs in WPF. Here's a way to approach that problem:
Suppose that you want to build a property-sheet-like UI: a grid that displays a row for each property, with a prompt and an input control of some kind. To do this, you're going to need a collection of objects, with each item in the collection including properties that describe the property and its value. A simple design would be a class that exposes a Prompt property and a Value property and that implements change notification.
Once you have created and populated this collection, you can implement an ItemsControl that displays it in a grid:
<ItemsControl ItemsSource="{Binding Properties}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="PropertyViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Prompt"/>
<ColumnDefinition SharedSizeGroup="Value"/>
</Grid.ColumnDefinition>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
</Grid>
<Label Content="{Binding Prompt}"/>
<TextBox Grid.Column="1" Text="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is pretty simple - the most complicated thing about it is using Grid.IsSharedSizeScope so that all of the grids that this control creates use the same column widths. You could also use a ListView instead of an ItemsControl, though using a ListView for this introduces a bunch of issues surrounding focus and selection that you may not want to deal with.
Note that because of the magic that is WPF template matching, you could conceivably implement the Value property as an object, and create different templates to handle the different possible types of the Value property - just like a real property sheet does. To do this, you'd create a template for each type, e.g.:
<DataTemplate DataType="{x:Type System:String}">
<TextBox Text="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type System:DateTime}">
<DatePicker Value="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
etc. Then you'd change the template for the PropertyViewModel so that instead of showing the Value in a TextBox, it uses a ContentPresenter, e.g.:
<ContentPresenter Grid.Column="1" Content="{Binding}"/>

Padding text in listbox

var a1 = "HEL";
var a2 = "HELLO";
var a3 = "LLO";
var length = a2.Length+5;
listbox.Items.Add(a1.PadRight(length) +"End");
listbox.Items.Add(a2.PadRight(length) + "End");
listbox.Items.Add(a3.PadRight(length) + "End");
I have code like this to obviously pad all text so that the word End lines up.
The problem is I have to change the font from the wpf listbox from Segoe UI to Courier New to have this work. The rest of my app uses Segoe UI, so I think it looks weird here.
Is there any way to achieve the result with Segoe UI or maybe a similar font with correct spacing I could use, or maybe someone has some other smart solution i haven't even thought of? :-)
Thanks
edit
at the end of the day I want this to display to related items like this:
ITEM A -> ITEM B
ITEM X -> ITEM Y
ITEM C -> ITEM E
dont want to use gridview.
Feed the ListBox the two pieces of data separately, and use a data template. Here's how.
First, create a little class to represent each item you want to insert:
public class WordPair {
public string First { get; set; }
public string Second { get; set; }
}
(You probably already have a suitable class and/or collection in your application -- I assume those pairs of strings are coming from somewhere!)
Second, set your ListBox.ItemsSource to a collection of these things:
listBox.ItemsSource = new List<WordPair> {
new WordPair { First = "ITEM A", Second = "ITEM B" },
new WordPair { First = "ITEM X", Second = "ITEM Y" },
};
Again, this collection may already exist in your app.
Third, create a DataTemplate specifying the desired layout, and assign it to your ListBox.ItemTemplate:
<!-- in your Window.Resources section -->
<DataTemplate x:Key="AlignedPairs">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding First}" Grid.Column="0" />
<TextBlock Text="->" TextAlignment="Center" Grid.Column="1" />
<TextBlock Text="{Binding Second}" TextAlignment="Right" Grid.Column="2" />
</Grid>
</DataTemplate>
<ListBox Name="listBox" ItemTemplate="{StaticResource AlignedPairs}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
(I've guessed at the exact alignment you want for the items, but you can obviously tweak it.)
Note that you also need to set the HorizontalContentAlignment of the ListBoxItems to Stretch using ListBox.ItemContainerStyle. Otherwise each ListBoxItem will take up only the space it needs, resulting in all the Grid columns being minimal size and looking like a straight concatenation. Stretch makes each ListBoxItem fill the full width so the Grid columns are forced to grow accordingly.
<ListBox
x:Name="listBox" HorizontalContentAlignment="Right"/>
check it :)

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.

New to WPF - What Control to Use / Getting Started?

I'm a WPF n0ob and I'm struggling with selecting the appropriate control to get the layout I want.
What I'm trying to do is draw a bunch of squares (virtual post-it notes) onto the screen. Each note is going to be a decent size (~150 pixels or so) and there could be hundreds of these notes. I want the whole thing to be scrollable so that you can resize the window however you like and the whole thing should be zoomable.
I've done this and it works.
But what I've done seems awfully wrong....
In the code, I'm dynamically creating post it notes and adding them to a giant canvas. I'm manually doing the math to determine where to place each note and how big the canvas should be. I added some labels at the top and had to go back and add a 'Y Offset' value to push all the squares down. I actually generate three different canvas controls and then add each one of them to a stack panel that is inside of a ScrollViewer. I added a scroll bar and set the the stack panel to zoom in and out as you adjust the bar.
It 'works', but I feel like I'm really not using WPF the way it's meant to be used. I tried achieving the same thing with a grid, but the grid didn't seem to want to size itself appropriately.
Can someone tell me a 'better' way to achieve the same look?
Here's my Xaml code - as you can see; there isn't much to it....
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition />
</Grid.RowDefinitions>
<Slider x:Name="ZoomSlider" Minimum="0.01" Value="1" Maximum="2" Margin="0,0,0,6" />
<ScrollViewer x:Name="MyScroller" Grid.Row="1" HorizontalScrollBarVisibility="Visible" HorizontalContentAlignment="Center" VerticalContentAlignment="Center">
<StackPanel x:Name="TicketsGrid" Background="White" HorizontalAlignment="Center">
</StackPanel>
</ScrollViewer>
</Grid>
And then here is what I'm doing in code (ugly!!!)
For Each myWorkItem As WorkItem In myWorkItems
Dim newRect As New Border
newRect.Width = TicketSizeX
newRect.Height = TicketSizeY
If myWorkItem.State.ToUpper.Contains("HOLD") Then
newRect.Background = New SolidColorBrush(Colors.Purple)
Else
newRect.Background = New SolidColorBrush(Color)
End If
newRect.CornerRadius = New System.Windows.CornerRadius(5)
newRect.BorderThickness = New System.Windows.Thickness(1)
newRect.BorderBrush = New SolidColorBrush(Colors.Black)
Dim myPanel As New StackPanel
newRect.Child = myPanel
Dim lblTitle As New Label
lblTitle.Content = myWorkItem.Id
lblTitle.FontWeight = System.Windows.FontWeights.Bold
Dim lblDesc As New TextBlock
lblDesc.Text = myWorkItem.Title
lblDesc.TextWrapping = TextWrapping.Wrap
myPanel.Children.Add(lblTitle)
myPanel.Children.Add(lblDesc)
newRect.SetValue(Canvas.LeftProperty, CType(((TicketCount Mod TicketsXPerUser) * TicketStepX) + (xOffset * TicketStepX * TicketsXPerUser), Double))
newRect.SetValue(Canvas.TopProperty, CType(((Math.Floor((TicketCount / TicketsXPerUser)) * TicketStepY)) + NameLabelHeight, Double))
myCanvas.Children.Add(newRect)
TicketCount += 1
Next
MyCanvas.Width = (TicketStepX * TicketsXPerUser) * myTFS.SharedCodeTeam.Count
MyCanvas.Height = (CType(((Math.Floor((MaxTicket / TicketsXPerUser)) + 1) * TicketStepY), Double))
TicketsGrid.Children.Add(MyCanvas)
ScrollViewer with an ItemsControl inside.
Bind the ItemsSource property of the ItemsControl to an ObservableCollection<PostIt> (where PostIt is a plain old CLR object with all the info that goes on the post it).
Add a DataTemplate to the ItemsTemplate property of the ItemsControl
Add controls to the DataTemplate and bind them directly to an instance of PostIt
Add PostIt instances to the ObservableCollection<PostIt> in your code.
The ScrollViewer handles all scrolling. That's all you need.
The ItemsControl is designed to bind against a collection. For each instance in the collection, it figures out what DataTemplate to use, creates a copy of the template, sets the root's DataContext to the instance it pulled from the collection, and adds the template to itself. It does this for each instance found in the collection.
In your codebehind, all you need to do is create a bunch of PostIts and add them to the collection. No godawful construction of UI elements like you're doing. Urgh.
If you can grasp this concept, you are a step away from understanding MVVM, the Model-View-Controller pattern in WPF. Read about it, try it out. Its a very simple way of making very complex applications with complex UI but with a minimum of code (and none of that crap you're doing currently).

In a WPF ListBox with more than 1000 Image Items the Zoom Images become slow

I met a problem when deveoping a photo viewer application.
I use ListBox to Show Images, which is contained in a ObservableCollection.
I bind the ListBox's ItemsSource to the ObservableCollection.
<DataTemplate DataType="{x:Type modeldata:ImageInfo}">
<Image
Margin="6"
Source="{Binding Thumbnail}"
Width="{Binding ZoomBarWidth.Width, Source={StaticResource zoombarmanager}}"
Height="{Binding ZoomBarWidth.Width, Source={StaticResource zoombarmanager}}"/>
</DataTemplate>
<Grid DataContext="{StaticResource imageinfolder}">
<ScrollViewer
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ListBox Name="PhotosListBox"
IsSynchronizedWithCurrentItem="True"
Style="{StaticResource PhotoListBoxStyle}"
Margin="5"
SelectionMode="Extended"
ItemsSource="{Binding}"
/>
</ScrollViewer>
I also bind the Image'height in ListBox with a slider.(the slider's Value also bind to zoombarmanager.ZoomBarWidth.Width).
But I found if the collection become larger, such as: contains more then 1000 images, If I use the slider to change the size of iamges, it become a bit slow.
My Question is.
1. Why it become Slow? become it tries to zoom every images,or it just because notify("Width") is invoked more than 1000 times.
2. Is there any method to solve this kind of problem and make it faster.
The PhotoListBoxStyle is like this:
<Style~~ TargetType="{x:Type ListBox}" x:Key="PhotoListBoxStyle">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}" >
<WrapPanel
Margin="5"
IsItemsHost="True"
Orientation="Horizontal"
VerticalAlignment="Top"
HorizontalAlignment="Stretch" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style~~>
But If I use the Style above, I have to use ScrollViewer outside ListBox, otherwise I have no idea how to get a smooth scrolling scrollerbar and the wrappanel seems have no default scrollerbar. Anyone help? It is said listbox with scrollviewer has poor performance.
The problem is that your new Layout Panel is the WrapPanel and it doesn't support Virtualization! It is possible to create your own Virtualized WrapPanel... Read more here
Also read more about other issues like the implementation IScrollInfo here
I also highly recommend that your do not create a new control template just to replace the layout panel... Rather do the following:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
The advantage of doing this is that you do not need to wrap your listbox in a scrollviewer!
[UPDATE] Also read this article by Josh Smith! To make the WrapPanel wrap... you also have to remember to disable horizontal scrolling...
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
I am not familiar with this component, but in general there is going to be limitations on the number of items a listbox can display at one time.
A method to solve this kind of problem is to keep the number of images loaded in the control within the number the control can display at acceptable performance levels. Two techniques to do this are paging or dynamic loading.
In paging, you add controls to switch between discrete blocks of pictures, for example, 100 at a time, with forward and back arrows, similar to navigating database records.
With dynamic loading, you implement paging behind the scenes in such a way that when the user scrolls to the end, the application automatically loads in the next batch of pictures, and potentially even removes a batch of old ones to keep the responsiveness reasonable. There may be a small pause as this occurs and there may be some work involved to keep the control at the proper scroll point, but this may be an acceptable trade-off.
I would recommend you not bind the Width/Height property of each individual image, but rather you bind a LayoutTransform on the ListBox's ItemsPanel. Something like:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel>
<StackPanel.LayoutTransform>
<ScaleTransform
ScaleX="{Binding Path=Value, ElementName=ZoomSlider}"
ScaleY="{Binding Path=Value, ElementName=ZoomSlider}" />
</StackPanel.LayoutTransform>
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
Part of the problem is that it is loading the full image in each. You have to use an IValueConverter to open each image in a thumbnail size by setting either the DecodePixelWidth or DecodePixelHeight properties on the BitmapImage. Here's an example I use in one of my projects...
class PathToThumbnailConverter : IValueConverter {
public int DecodeWidth {
get;
set;
}
public PathToThumbnailConverter() {
DecodeWidth = 200;
}
public object Convert( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture ) {
var path = value as string;
if ( !string.IsNullOrEmpty( path ) ) {
FileInfo info = new FileInfo( path );
if ( info.Exists && info.Length > 0 ) {
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.DecodePixelWidth = DecodeWidth;
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.UriSource = new Uri( info.FullName );
bi.EndInit();
return bi;
}
}
return null;
}
public object ConvertBack( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture ) {
throw new NotImplementedException();
}
}
try to virtualize your stackpael with the VirtualizingStackPanel.IsVirtualizing="True" attached property. this should increase performance.
using a listbox with many items in a scrollviewer is another known performance issue within wpf. if you can, try to get rid of the scrollviewer.
if your itemtemplates are kinda complex you should consider using the Recycling VirtualizationMode. this tells your listbox to reuse existing objects and not create new ones all the time.
What does your PhotoListBoxStyle style look like? If it's changing the ListBox's ItemsPanelTemplate then there's a good chance your ListBox isn't using a VirtualizingStackPanel as its underlying list panel. Non-virtualized ListBoxes are a lot slower with many items.

Resources