Displaying a list of user controls in flow document - wpf

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.

Related

Paginating a FlowDocument

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.

ListBox with copy paste feature

I am using MVVM approach, my UI needs to provide a simple copy paste area (something like TextBox) to the user and once user enters the text, I need to convert all newline separated text into list items - list items because I want to highlight individual items with errors, if any.
It should be seamless for the user- user should only see List items and should be able to edit them later.
Hence, basically I need a ListBox (for the programmer) which acts like a textbox (for the user). How can I achieve this?
I have implemented this using a Grid with TextBox & ListBox in the same Row, but with some overlap so that user can paste text in textbox and as soon as user enters the text, viewmodel separates the newlines into listitems. It is working but I am facing many issues (like clearing the textbox, etc.). I am looking for better more efficient solution if any. Below is the code snippet from xaml:
<Grid>
<TextBox AcceptsReturn="True" VerticalAlignment="Stretch" BorderBrush="Transparent"
Text="{Binding TextBoxData, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding OnNewLineCommand}"/>
</TextBox.InputBindings>
</TextBox>
<ListBox ItemsSource="{Binding ListBoxItems}" Margin="0,20,0,0" BorderBrush="Transparent" FocusVisualStyle="{x:Null}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding}" BorderBrush="Transparent"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
</Grid>
Thanks,
RDV

How do I access a windows control (checkbox) that is created dynamically in a WPF treeview?

I have a WPF Tree View with hierarchical data templates that loads objects and displays them fine. Within the children of the treeview, I am showing the "Name" of the object in the Tree View using a TextBlock, along with a Check Box next to it. Here is my code for reference:
<DockPanel Name="test1" Margin="10,10,0,10" VerticalAlignment="Stretch" Grid.Row="3" Grid.RowSpan="7" Grid.Column="0">
<DockPanel.Resources>
<local:CheckBoxCommand x:Key="cbc"></local:CheckBoxCommand>
<src:TreeViewFilter x:Key="MyList" />
<HierarchicalDataTemplate DataType="{x:Type src:TreeViewParent}" ItemsSource="{Binding Path=OrderAttributes}">
<TextBlock Text="{Binding Path=NameAndCount}" FontSize="24"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type src:OrderAttribute}" ItemsSource="{Binding Path=OrderAttributes}">
<StackPanel Name="test" Orientation="Horizontal" VerticalAlignment="Center">
<CheckBox Command="{StaticResource cbc}"
CommandParameter="{Binding Path=NameAndParent}" Visibility="{Binding Path=CheckBoxVisible}" VerticalAlignment="Center">
</CheckBox>
<TextBlock Text="{Binding Path=NameAndCount}" FontSize="16"/>
</StackPanel>
</HierarchicalDataTemplate>
</DockPanel.Resources>
<TreeView Name="treeView1" BorderThickness="2" ItemsSource="{Binding Source={StaticResource MyList}, UpdateSourceTrigger=PropertyChanged}" TreeViewItem.Selected="filterByBatchStatus"/>
</DockPanel>
When A user checks a checkbox in the tree, certain stuff happens in my application based on which checkbox is checked. The way I know which checkbox is being checked is by passing a paramater through a command, and that parameter is bound to the "NameAndParent" of the object. All of this works fine.
My problem begins when I give the user the option to save which checkboxes have been checked and I save the "Name" of each object next to the checked box into an XML. As you can see, I am only saving the "Name" of the object, but this name has no hook to the checkbox, So I can't go back and "find" the associated checkbox.
When I give the user the option to load one of these saved files, I want to traverse the tree and check the boxes that were saved. The problem is that the checkboxes DONT HAVE A NAME, OR UID, and I cant assign them one through binding because that is not allowed.
Is there anyway to traverse the tree view and somehow compare the saved name to the name of each element child in the tree, and then check that specific checkbox, Or is this something that has to be programmed in a different way?
Just create a boolean IsChecked property in the class that contains your Data, and bind the CheckBox.IsChecked to that. instead of having to manipulate the view, you can more easily manipulate the data it is bound to, removing the need for fancy Visual-Tree operations, and removing the dependency between your application logic and your UI. This is the most important realization of the Model-View-ViewModel Pattern.
If you do not want to introduce UI-related logic (such as the IsChecked property I mentioned) into your Data Model, you will have to introduce a ViewModel in between the Model and the View.

showing different user controls - WPF MVVM

I created a dbml file, which automatically created the designer.cs
In the designer.cs (which is the Model in the MVVM) there are two different classes in the database:
ElementA and ElementB.
I have two user controls:
Element_A_UserControl - displays a single instance of ElementA
Element_B_UserControl - displays a single instance of ElementB
There is another user control that has two stack panels.
The first stack panel displays a list of Element_A_UserControl
The second stack panel displays a list of Element_B_UserControl
Here is the stack panel #1 XAML:
<StackPanel>
<ItemsControl ItemsSource="{Binding AllElements_A}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<vw:Element_A_UserControl x:Name="elementA">
</vw:Element_A_UserControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Here is the stack panel #2 XAML:
<StackPanel>
<ItemsControl ItemsSource="{Binding AllElements_B}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<vw:Element_B_UserControl x:Name="elementB">
</vw:Element_B_UserControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Up to now everything works fine.
I want to have one stack panel, which displays a list of ElementA or a list of ElementB depending of a condition.
Note: The property to get list of elements is different.
i.e.
ItemsSource="{Binding AllElements_A}
ItemsSource="{Binding AllElements_B}
I hope that my question is clear enough.
Dazy.
One way is that you could try to use a conditional DataTemplate. Something like this:
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:ElementAType}">
<vw:Element_A_UserControl x:Name="elementA">
</vw:Element_A_UserControl>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ElementBType}">
<vw:Element_B_UserControl x:Name="elementB">
</vw:Element_B_UserControl>
</DataTemplate>
</ItemsControl.Resources>
Then, in your viewmodel, create:
public ObservableCollection<object> CombinedCollection {get; set;}
and load it with either of your collections conditionally.
Alternatively, keep both ItemsControls in your XAML, and conditionally hide/show them using Visibility and a BooleanToVisibilityConverter. Given these two choices, I would likely choose this one, as it is clearer in the code, and easier to maintain than the conditional DataTemplate above. However, you seemed to indicate that you didn't want to do that, so I presented the first one as an option.

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

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

Resources