I want to be able to create "reports" as regular MVVM views, so to test this idea I created the following view. It just consists of a bound textblock, an ItemsControl (bound to a collection of a few hundred strings), and a second text block:
<UserControl ...>
<UserControl.Resources>
<DataTemplate x:Key="myListItemTemplate">
<TextBlock Text="{Binding}"
Foreground="Blue" />
</DataTemplate>
</UserControl.Resources>
<FlowDocument x:Name="flowDoc">
<BlockUIContainer>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Message}" />
<ItemsControl ItemsSource="{Binding SomeList}"
ItemTemplate="{StaticResource myListItemTemplate}" />
<TextBlock Text="THE END" />
</StackPanel>
</BlockUIContainer>
</FlowDocument>
</UserControl>
I print the view using a DocumentPaginator like this:
private void Print(IPrintableView view)
{
var dlg = new PrintDialog();
if (!dlg.ShowDialog().GetValueOrDefault())
{
return;
}
var paginator = new MyDocumentPaginator(
view.FlowDocument,
new Size(dlg.PrintableAreaWidth, dlg.PrintableAreaHeight),
new Size(DefaultMarginWidthDpi, DefaultMarginHeightDpi));
dlg.PrintDocument(paginator, "My print job");
}
Unsurprisingly the document isn't being paginated, and produce a 1-page report that gets truncated partway through the list. (I don't believe my document paginator implementation is important here, which simply adds a header and footer to each page, and I see the same issue if I use the FlowDocument's own FlowDocumentPaginator).
As I now understand it, document paginators will only break on "Block" controls, so I modified the xaml to wrap each control in a BlockUIContainer:
<UserControl ...>
<UserControl.Resources>
<DataTemplate x:Key="myListItemTemplate">
<TextBlock Text="{Binding}"
Foreground="Blue" />
</DataTemplate>
</UserControl.Resources>
<FlowDocument x:Name="flowDoc">
<BlockUIContainer>
<TextBlock Text="{Binding Message}" />
</BlockUIContainer>
<BlockUIContainer>
<ItemsControl ItemsSource="{Binding SomeList}"
ItemTemplate="{StaticResource myListItemTemplate}" />
</BlockUIContainer>
<BlockUIContainer>
<TextBlock Text="THE END" />
</BlockUIContainer>
</FlowDocument>
</UserControl>
Here, a page break now occurs after the first TextBlock, with the list starting on page 2 (but still being truncated at the end of that page), and the second textblock appearing on page 3.
I assume the paginator is measuring and rendering the entire ItemsControl as a whole, so I then tried placing each list item inside a BlockUIContainer via the ItemContainerStyle:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<BlockUIContainer>
<ContentPresenter Content="{Binding}" />
</BlockUIContainer>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
This made no difference though. Is there a way to get this working? Am I even approaching this in the right way? Placing every UI control inside a "block" seems very cumbersome, especially when it the time comes to implement a real report containing a lot of complexity. The real reports won't necessarily be list based either, and are more likely to contain a mixture of controls - text, images, lists, etc. in a fairly free-style layout (more web page than banded report).
So this wasn't straightforward as I was expecting, and if it were not for stumbling across this article then I'd probably never have found a solution. In the article, the author has created a series of classes and controls including a "FlowDocument friendly" list, with the option of rendering it as a Table, meaning that page breaks will occur between the items' TableRows when necessary.
Here's my updated "report view" xaml. Note that I've also refactored the text by using a <Paragraph> rather than a TextBlock inside a BlockUIElement. You'll also notice that these Paragraphs are also bindable, via a custom BindableRun control that is also part of the above article:
<UserControl ...>
<FlowDocument x:Name="flowDoc"
TextElement.FontFamily="Arial">
<Paragraph Margin="0,0,0,20">
<flowDocuments:BindableRun BoundText="{Binding Message}" />
</Paragraph>
<flowDocuments:ItemsContent ItemsSource="{Binding SomeList}">
<flowDocuments:ItemsContent.ItemsPanel>
<DataTemplate>
<flowDocuments:Fragment>
<Table>
<Table.Columns>
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup flowDocuments:Attached.IsItemsHost="True">
<TableRow Background="LightBlue">
<TableCell>
<Paragraph>Heading</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</flowDocuments:Fragment>
</DataTemplate>
</flowDocuments:ItemsContent.ItemsPanel>
<flowDocuments:ItemsContent.ItemTemplate>
<DataTemplate>
<flowDocuments:Fragment>
<TableRow>
<TableCell>
<Paragraph>
<flowDocuments:BindableRun BoundText="{Binding}" />
</Paragraph>
</TableCell>
</TableRow>
</flowDocuments:Fragment>
</DataTemplate>
</flowDocuments:ItemsContent.ItemTemplate>
</flowDocuments:ItemsContent>
<Paragraph>THE END</Paragraph>
</FlowDocument>
</UserControl>
The custom ItemsContent control is used to render the list, and has a few familiar properties:
an ItemsSource property for binding to the VM's collection
an ItemTemplate property defining a DataTemplate that consists of a "Fragment" element (again from the above article), and within this a <TableRow> used to render the string items in my VM's collection
an ItemsPanel, used to define the "container" - in this case a Table control. This example includes a hardcoded TableRow used to display the column headings. I believe other containers are permitted such as a , although I didn't read the article in detail.
Anyway, it all appears to work, and produces a report that is correctly paginated, with the list of strings breaking to new pages at the correct places.
If anyone else uses the article to generate reports as I have done, be aware that the ItemsContent Loaded event never fires, resulting in the list not rendering (this is presumably due to the way I'm programmatically creating the view and/or not displaying it on screen). To get it working, I had to comment out the three if (IsLoaded) lines that appear later on in this class.
Related
so I have a problem with an XAML file I'm using; I'm trying to use a DataGrid to add a table view of the properties for an element the user selects. How I'm currently attempting to do this is that I have a list containing the appropriate pairs that gets populated on user click, and then the ItemsSource is set to that list. I have tried changing the details of this implementation (binding the ItemsSource without a reference to the datagrid itself, etc, but sooner or later they all seem to hit the same error)
The weird thing (to me) is that after a few clicks on different elements (and hitting 'continue' when the exception pops up) the grid does populate with data, although it often seems to "freeze" (showing the same data for a few elements before finally refreshing a couple of elements later, no exceptions are thrown, but the behaviour is definitely inconsistent)
.xaml.cs
// ParameterPair is a custom class that contains 2 string fields (name, value)
public List<ParameterPair> AllParameters { get; private set; } = new List<ParameterPair>();
// called (only) when a new element is click
// ... code to populate AllParameters here
// definitely populates properly, checked through debugging
this.dGrid.ItemsSource = AllParameters;
.xaml
<Page ...>
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TabControl>
<TabItem Header="Add Constraint">
<Grid Name="loginBlock" Grid.Row="0">
<GroupBox Header="Properties"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="10, 10, 10, 0">
<StackPanel>
<controls:DataGrid x:Name="dGrid"
Height="300" Margin="12"
AutoGenerateColumns="true"
ItemsSource="{Binding}"
/>
</StackPanel>
</GroupBox>
</Grid>
</TabItem>
<TabItem Header="Manage Constraints" />
</TabControl>
</Grid>
</Page>
The error doesn't seem to be from your databinding, but from the xaml markup somewhere.
Your GroupBox doesn't seem to have closing brackets.
And is this a custom DataGrid? Since it's referenced as "controls:DataGrid" unlike your other controls. There might be something wrong in its markup.
Okay, so I fixed it. Turns out AutoGenerateColumns="true" was the culprit. I came upon this thread in my frustration, and tried manually setting the columns, which seems to work perfectly fine, just binding the columns to the appropriate fields in the class being used for the list.
<DataGrid x:Name="dGrid"
Height="300" Margin="12"
AutoGenerateColumns="false"
ItemsSource="{Binding}"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</DataGrid.Columns>
</DataGrid>
P.S. I will avoid choosing this as my answer as it is largely a workaround, hopefully someone more experienced will come and give a more thorough and concise solution (maybe myself later on in this project...)
I need to create a printable representation of my data and since I have created some control to edit the data entities I thought I could drop that into a flow document one after the other using a listbox or something similar and a get a first printable version. I made some proof-of-concept via:
<FlowDocumentReader>
<FlowDocument x:Name="flowDoc" PageWidth="793" PageHeight="1122" Background="AntiqueWhite" PagePadding="48" ColumnWidth="793">
<Paragraph>
<v:DisplayEntityV DataContext="{Binding MySample, Mode=OneWay, Source={StaticResource AppDataContext}}" />
</Paragraph>
</FlowDocument>
</FlowDocument>
If I change the paragraph like this
<Paragraph>
<ListBox ItemsSource="{Binding MyListOfSamples, Mode=OneWay, Source={StaticResource AppDataContext}}">
<ListBox.ItemTemplate>
<DataTemplate>
<v:DisplayEntityV DataContext="{Binding Mode=OneWay}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Paragraph>
I get only one paragraph and there is no page break between the DisplayEntityV where appropriate. I got to the conclusion that for my case it must be the other way round: the inner structure should be the paragraph and the outer structure should be a list, but I can use only List within FlowDocument which creates a bulleted list and has no data binding capabilities.
I really would prefer to have something like a listbox to which I can bind my items I want to display (because that gives me the advantage to see at design time how it will look like at least with sample data) rather than adding the items in code behind at run time. Any suggestions are highly welcome.
I have a WPF application in .NET 3.5 SP1 which is using TabControl.
In that we have TabItems, which in turn have their Styles to determine currently displayed items.
Let's say we have a TabItem named Books, now Books will have three stages of display:
1. Loading results,
2. Displaying results,
3. Displaying no results - i.e. nothing found.
<TabControl>
<TabItem Header="Books"/>
<TabItem Header="DVD's"/>
...
</TavControl>
Now I have 5 TabItems which let's say represent "DVD's", "Blu-Rays", "CD's", "Books" and "Comics".
<TabItem Header="Books">
<Control>
<Control.Resources>
<Style TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ListView ItemsSource="{Binding Books}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<!-- Assign different Visuals depending on the current state of the app, i.e. Loading, No results, results found
<DataTrigger .../>
</Style.Triggers>
</Style>
</Control.Resources>
</Control>
</TabItem>
Underneath the TabItem I have a TextBlock to display number of currently found results:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Displaying {0} records for your Shop ({1})" Converter="{StaticResource tstMVC}">
<Binding ElementName="Tc" Path="SelectedValue"/>
<Binding Path="ShopId" FallbackValue="Liverpool"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Converter is there for me to check what values are passed in the MultiBinding.
ISSUE:
When user selects a tab item I would like to display current number of the items displayed but I can't locate the ListView in the Control, as that's the Current Content of the TabItem.
I have tried TabControl.SelectedItem, SelectedValue and still can't find Current ItemsSource.Count.
Thanks in advance
UPDATE:
I have tried both of the solutions big thanks to #Sheridan and #pushpraj!
Unfortunately I haven't used either of them, instead I used ListView inside of the TabItem and then accessed it with this code:
<TabControl Name="Tc">
<TabItem><ListView ItemsSource="{Binding Books}"/></TabItem>
...
</TabControl>
<TextBlock Text="{Binding ElementName=Tc, Path=SelectedItem.Content.Items.Count}"/>
This way Content of my TextBlock changes every time user selects different Tab.
P.S. Nevertheless I wouldn't have done it without evaluation of both answers.
if you have separate collection classes for all you entities you may use the following approach
define data templates for your collection classes
eg
<DataTemplate DataType="{x:Type l:BookCollection}">
<ListView ItemsSource="{Binding}" />
</DataTemplate>
xaml
<TabControl x:Name="Tc">
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
or if you do not have separate collections then use DataTemplate as follows
<TabControl x:Name="Tc">
<TabControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
then the binding to get the selected tab's items count will be
<TextBlock Text="{Binding SelectedItem.Content.Count, ElementName=Tc}" />
In WPF, we generally work with data elements rather than UI elements. By that, I mean that it is customary to data bind data elements from our code behind or view models to the UI elements in the views, UserControls and Windows.
Therefore, if you have a data collection property named Books in your code behind or view model, then you can simply refer to that collection to find out how many items are in it, rather than trying to find it via the UI controls:
<ListView ItemsSource="{Binding Books}" />
You could even expose the count of items as a separate property and data bind to it directly:
public ObservableCollection<Book> Books
{
get { return books; }
set
{
books = value;
NotifyPropertyChanged("Books");
NotifyPropertyChanged("BookCount");
}
}
public int BookCount
{
get { return Books.Count; }
}
UPDATE >>>
In response to your latest comment, you can find out how to access UI elements from within a ControlTemplate from the How to: Find ControlTemplate-Generated Elements page on MSDN. In short though, you need to access the element that has the ControlTemplate applied (the relevant TabItem in your case) and then you can use the FrameworkTemplate.FindName Method to find the internally declared elements. Take this example from the linked page:
// Finding the grid that is generated by the ControlTemplate of the Button
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
// Do something to the ControlTemplate-generated grid
MessageBox.Show("The actual width of the grid in the ControlTemplate: "
+ gridInTemplate.GetValue(Grid.ActualWidthProperty).ToString());
I am using a StackPanel to layout several controls vertically (ie, Title, sub titles, listbox, separator, listbox, etc).
The StackPanel is a child of a ScrollViewer to ensure its content is always scrollable.
One of the controls in the StackPanel is a ListBox.
Its ItemsSource is data bound to a huge collection, and a complex DataTemplate is used to realise each item.
Unfortunately, I'm getting really poor performance (high cpu/memory) with it.
I tried
setting the ListBox's ItemsPanel to a VirtualizingStackPanel, and
overriding its ControlTemplate to only an ItemsPresenter (remove the ListBox's ScrollViewer).
But there were no difference in performances. I'm guessing the StackPanel gives its internal children infinite height during measure?
When I replaced the ScrollViewer and StackPanel with other panels/layouts (e.g, Grid, DockPanel) and the performance improves significantly, which leads me to believe the bottleneck, as well as solution, is in virtualization.
Is there any way for me to improve the cpu/memory performance of this view?
[Update 1]
Original Sample project: http://s000.tinyupload.com/index.php?file_id=29810707815310047536
[Update 2]
I tried restyling/templating TreeView/TreeViewItems to come up with the following example. It still takes a long time to start/same,high memory usage. But once loaded, scrolling feels a lot more responsive than the original sample.
Wonder if there's any other way to further improve the start up time/memory usage?
Restyled TreeView project: http://s000.tinyupload.com/index.php?file_id=00117351345725628185
[Update 2]
pushpraj's solution works like a charm
Original:
Startup: 35s,
Memory: 393MB
Scrolling: Slow
TreeView:
Startup: 18s,
Memory 377MB,
Scrolling: Fast
pushpraj's solution:
Startup: <1s,
Memory: 20MB,
Scrolling: Fast
you may perhaps limit the maximum size of the huge list box and enable Virtualization
eg
<ListBox MaxHeight="500"
VirtualizingPanel.IsVirtualizing="true"
VirtualizingPanel.VirtualizationMode="Recycling" />
this will enable the ListBox to load a few items only and will enable a scrollbar on listbox to scroll to rest of the items if needed.
at the same time setting VirtualizationMode to Recycling will help you to reuse the complex data templates thus eliminating the need of re creating them again for every item.
EDIT
here is a solution based on your sample, I have used CompositeCollection with Virtualization to achieve the desired.
xaml
<Grid xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:l="clr-namespace:PerfTest">
<Grid.Resources>
<DataTemplate DataType="{x:Type l:Permission}">
<StackPanel Orientation="Horizontal">
<CheckBox />
<TextBlock Text="{Binding Name}" />
<Button Content="+" />
<Button Content="-" />
<Button Content="..." />
</StackPanel>
</DataTemplate>
<CompositeCollection x:Key="data">
<!-- Content 1 -->
<TextBlock Text="Title"
FontSize="24"
FontWeight="Thin" />
<!-- Content 2 -->
<TextBlock Text="Subtitle"
FontSize="16"
FontWeight="Thin" />
<!-- Content 3 -->
<CollectionContainer Collection="{Binding DataContext, Source={x:Reference listbox}}" />
<!-- Content 4 -->
<TextBlock Text="User must scroll past the entire list box before seeing this"
FontSize="16"
FontWeight="Thin"
Padding="5"
TextWrapping="Wrap"
Background="#99000000"
Foreground="White" />
</CompositeCollection>
</Grid.Resources>
<ListBox x:Name="listbox"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{StaticResource data}" />
</Grid>
code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var items = new ObservableCollection<Permission>();
foreach (var i in Enumerable.Range(0, 10000).Select(i => new Permission() { Name = "Permission " + i }))
{ items.Add(i); }
DataContext = items;
}
}
public class Permission
{
public string Name { get; set; }
}
since we can not create data template for string so I changed the string collection to Permission collection. I hope in your real project it would be something similar.
give this a try and see if this is close to what you need.
note: you may safely ignore if there is any designer warning on Collection="{Binding DataContext, Source={x:Reference listbox}}"
I am having issues with something that seems like it should be very simple but in fact has proven quite difficult.
Lets say you have a TabControl bound to an itemsource of ViewModels and the items displayed using a DataTemplate. Now lets say the DataTemplate consists of a Grid with two columns and a Grid splitter to resize the columns.
The problem is if you resize the columns on one tab, and switch to another tab, the columns are also resized. This is because the TabControl shares the DataTemplate among all tabs. This lack of UI persistence is applied to all elements of the template which can make for a frustrating experience when various UI components are adjusted. Another example is the scroll position in a DataGrid (on a tab). A DataGrid with few items will be scrolled out of view (only one row visible) if a DataGrid with more rows was scrolled to the bottom on another tab. On top of this, if the TabControl has various items defined in multiple DataTemplates the view is reset when you switch between items of differenet types. I can understand that this approach saves resources but the resultant functionality seems quite contradictory to expected UI behavior.
And so i'm wondering if there is a solution/workaround to this as i'm sure it's something that others have encountered before. I've noticed a few similar questions on other forums but there was no real solution. One about using the AdornerDecorator but that doesn't seem to work when used with a DataTemplate. I'm not keen on binding all the UI properties (like column width, scroll position) to my ViewModels and in fact I tried it for the simple GridSplitter example and I didn't manage to make it work. The width of the ColumnDefinitions were not necessarily affected by a grid splitter. Regardless, it would be nice if there were a general solution to this. Any thoughts?
If I ditch the TabControl and use an ItemsControl will I encounter a similar issue? Would it be possible to modify the TabControl Style so it doesn't share the ContentPresenter between tabs?
I've been messing with this on and off for a quite a while now. Finally, instead of trying to fix/modify the TabControl I simply recreated it's functionality. It's actually worked out really well. I made a Tab'like'Control out of a Listbox (Tab headers) and an ItemsControl. The key thing was to set the ItemsPanelTemplate of the ItemsControl to a Grid. A bit of Styling, and a DataTrigger to manage the Visibility of the Items and voila. It works perfect, each "Tab" is a unique object and preserves all it's UI states like scroll position, selections, column widths, etc. Any downsides or problems that might occur with this type of solution?
<DockPanel>
<ListBox
DockPanel.Dock="Top"
ItemsSource="{Binding Tabs}"
SelectedItem="{Binding SelectedTab}"
ItemContainerStyle="{StaticResource ImitateTabControlStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Orientation="Horizontal">
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel
Margin="2,2,2,0"
Orientation="Horizontal" >
<TextBlock
Margin="4,0" FontWeight="Bold"
Padding="2"
VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{Binding Name}" >
</TextBlock>
<Button
Margin="4,0"
Command="{Binding CloseCommand}">
<Image Source="/TERM;component/Images/Symbol-Delete.png" MaxHeight="20"/>
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ItemsControl
ItemsSource="{Binding Tabs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl
Content="{Binding}">
<ContentControl.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding IsSelected}" Value="False">
<Setter Property="ContentControl.Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>