WPF: Fast auto-sizing by height grid - wpf

I have to implement window containing several tables into one scroll viewer. This means that this tables should be stretched by the content. User able to add / remove items (rows) to these tables.
The concept was shown on the following mockup. The main problem is that grid should not have scroll bar, it should have dynamic height.
For the moment this interface is implemented as ItemsControl inside the ScrollViewer. In the ItemTemplate of the ItemsControl added DataGrid to represent tabular data.
<ScrollViewer CanContentScroll="True"
HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=Groups}"
VirtualizingStackPanel.IsVirtualizing="True">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="125" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0"
Style="{StaticResource ResourceKey=LabelBaseStyle}"
Content="{Binding Path=GroupLabel}"
HorizontalAlignment="Right"/>
<Button Content="{x:Static Member=Resources:CommonStrings.Delete}"
Style="{StaticResource ResourceKey=ButtonBaseStyle}"
VerticalAlignment="Center" />
<DataGrid Grid.Row="0" Grid.Column="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Items}">
<DataGrid.Columns>
<!-- 21 text columns here -->
</DataGrid.Columns>
</DataGrid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
This solution works fine until some amount of data has been added. Now on the 7 tables (grids) with about 50 items in the each the application is impossible slow: it takes several minutes to render it. After profiling I've found that almost all the time is spent on Measure and Arrange methods. Also I've found recomentation to not to use DataGrid into contrainers which measures it with infinity. So I undestand the problem, but I cannot change interface.
I've tried to write the same interface without DataGrid: replaced it by ItemsControl with TextBlocks. This solution works fine. But I've several similar interfaces and it's not looks very good to write so much similar code. The code shown below is replacement for DataGrid in previous sample:
<ItemsControl ItemsSource="{Binding Path=Items}"
VirtualizingStackPanel.IsVirtualizing="True"
Grid.Row="0" Grid.Column="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Type}" />
<!-- Another 20 TextBlocks here -->
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
So I need to implement own UserControl or find one ready to use.
Can anyone suggest me something? Maybe some workaround or lightweight control?

Re-did my answer....this is a better solution for your issue. I'm sure you can figure out how to modify this to work:
In my Main Window Xaml:
<ScrollViewer Height="Auto" Width="Auto" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Visible">
<StackPanel Height="Auto" Width="Auto">
<ItemsControl ItemsSource="{Binding ItemList}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type wpfmvvmtest:itemTypeA}">
<wpfmvvmtest:testUC></wpfmvvmtest:testUC>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</StackPanel>
</ScrollViewer>
Here's the Main ViewModel for the Main Window (testVM) all done in constructor for example:
public testVM()
{
ItemList = new ObservableCollection<object>();
Random rnd = new Random();
for (int i = 0; i < 3; i++)
{
itemTypeA item = new itemTypeA(rnd.Next(100));
ItemList.Add(item);
}
}
public ObservableCollection<object> ItemList { set; get; }
and here's what "itemTypeA" is like:
public itemTypeA(int count)
{
Items = new DataTable();
Items.Columns.Add("One");
Items.Columns.Add("Two");
Items.Columns.Add("Three");
Items.Columns.Add("Four");
Description = "Count = " + count;
for (int i = 0; i < count; i++)
{
DataRow dr = Items.NewRow();
dr[0] = i*1;
dr[1] = i * 2;
dr[2] = i * 3;
dr[3] = i * 4;
Items.Rows.Add(dr);
}
}
public DataTable Items { set; get; }
Here's the important part(Make sure you have no height/width properties set in your UserControl for the DataTemplate (That allows for the DataGrid to set the height/width):
<UserControl x:Class="wpfmvvmtest.testUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="d:designInstance itemTypeA"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Stretch" Content="{Binding Description}"></Label>
<Border Grid.Row="1" BorderBrush="Black" BorderThickness="2">
<DataGrid ItemsSource="{Binding Path=Items}">
</DataGrid>
</Border>
</Grid>
</UserControl>

Related

ItemsControl fill remainder of columns

We have a page which we use to display a lot of information at once.
On this page we want a readouts section in the top left, and then for the remainder of that columns, and for the rest of the page we want to display some sensor outputs. Layout is roughly the following.
What I have attemtped so far is as follows
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:ParameterDisplayPanel HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Columns="1"
Grid.Column="0"/>
<ItemsControl
Grid.Column="0"
Grid.ColumnSpan="3"
ItemsSource="{Binding ItemCollection, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<views:SensorControlView
DataContext="{Binding}"
Width="280"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
This returns a layout where the sensors overlap the readout panel.
If instead of using a grid I use a wrap panel, I get a row at the top which displays the readouts, and then all the sensor states start beneath that first row.
You could try sth. like this.
Note that this is not done yet. It's just a raw example.
Basically what I did is I added the SensorReadoutControl first and then added the remaining SensorItems to the same collection.
I then used a WrapPanel.
This is what it looks like (added some BackgroundColors to make more clear what control is where !)
So here is my example code:
XAML:
<Window x:Class="WpfApp1.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">
<Window.Resources>
<DataTemplate x:Key="SensorItemTemplate">
<Grid Background="Gray" Height="50">
<Border BorderThickness="1" BorderBrush="Pink"
Margin="5">
<TextBlock Text="{Binding Name}"
Width="200"
TextAlignment="Center"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Items}"
Background="Green"
ItemTemplate="{StaticResource SensorItemTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
ViewModel:
public class ViewModel
{
public ViewModel()
{
Grid grid = new Grid() { Width = 200, Height = 150, Background = Brushes.Blue };
grid.Children.Add(new TextBlock()
{
Text = "Sensor readouts" ,
VerticalAlignment = System.Windows.VerticalAlignment.Center,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
});
Items.Add(grid);
for (int i = 0; i < 50; i ++)
{
Items.Add(new SensorItem() { Name = $"SensorItem {i}" });
}
}
public List<object> Items { get; set; } = new List<object>();
}
public class SensorItem
{
public string ItemTypeName { get; set; } = "SensorItem";
public string Name { get; set; }
}
I guess that won't satisfy your needs but it may help you to progress.

WPF SelectedItem working inconsistently....(MVVM data binding)

I have a WPF ListBox that is a child of a ListView.
The ListBox has a 'SelectedItem' attribute that is bound to a property on my ViewModel SelectedTask.
When I click a row on my ListBox, then it updates SelectedTask. But only sometimes.....
By sometimes:
Every List Box row will update SelectedTask at least once
The ones in the "middle" of the screen will always update SelectedTask. I can hop from one row to another and back again, each time SelectedTask is updated
The ones at the top or bottom of the screen will update SelectedTask, but maybe only once. If I select another row and then return to this one, I can not get SelectedTask to be updated.
A highly cut-down/edited view of my XAML is as follows:
<ListView ItemsSource="{Binding WorkItems}" >
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="850"/>
</Grid.ColumnDefinitions>
/* Display properties from Work Item */
<TextBox Grid.Row="0" Grid.Column="0" Text="{Binding Code}" />
/* This SelectedItem works sometimes, but not all the time */
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Tasks}" SelectedItem="{Binding Path=DataContext.SelectedTask, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,0,0,5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
/* Display properties from Work Item Task */
<TextBlock Text="{Binding Path=Description}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
EDIT
I was asked what the SelectedTask property looked like:
public MyTaskType SelectedTask
{
get { return _selectedTask; }
set
{
_selectedTask = value;
NotifyPropertyChanged();
}
}

How can I get SelectedItem and show Headers using ItemsControl?

I am working on WPF Windows Application. I'm using ItemsControl to show collection list. Working on this I found there is no SelectedItem property in ItemsControl. Then how can I get the Selected Item from the ItemsControl. And also How can I display the Headers of ItemsControl.
<ItemsControl ItemsSource="{Binding CustomSalesProducts, Mode=TwoWay}">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<Border>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CanHorizontallyScroll="True" CanVerticallyScroll="True" Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="SalesGrid" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<controls:HeaderedContentControl Header="{Binding ProductName, Mode=TwoWay}" Margin="{DynamicResource Margin4}" Style="{DynamicResource HeaderedContentControlStyle}" HorizontalContentAlignment="Right">
</controls:HeaderedContentControl>
<TextBox Text="{Binding OrderQty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" Margin="{StaticResource Margin4}" Style="{DynamicResource MiniTextBoxStyle}" ToolTip="Quantity" />
<TextBlock Text="{Binding UnitSalePrice, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="1" Margin="{StaticResource Margin4}" ToolTip="Price"/>
<TextBox Text="{Binding Discount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="2" Grid.Row="1" Margin="{StaticResource Margin4}" Style="{DynamicResource MiniTextBoxStyle}" ToolTip="Discount"/>
<TextBlock Text="{Binding TaxAmount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="3" Grid.Row="1" Margin="{StaticResource Margin4}" ToolTip="Tax Amount"/>
<TextBlock Text="{Binding LineTotal, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="4" Grid.Row="1" Margin="{StaticResource Margin4}" ToolTip="Total"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Thanks,
As you said there is no SelectedItem in the ItemsControl. You can use ListBox instead.
A bit late to the party, but I came across the same problem, and in the hopes that this can help someone else here's how I rolled my own SelectedItem since I didn't want to use a ListBox.
You could expose a SelectedCustomSalesProduct property to the class you are using as your DataContext, and then you can keep track of the selected item yourself by setting it when the item is selected.
In your SalesGrid, you could add event handlers for the MouseLeftButtonDown and for the TouchDown events, and use the Tag property to keep a reference to the item being rendered as such:
Please note that in my case, I was using a StackPanel instead of a Grid, and I didn't compile the code below, use it for illustrative purposes.
By using this example you should be able to get the general idea and set the Selected item in your business service.
<DataTemplate>
<Grid x:Name="SalesGrid" Background="White"
Tag="{Binding}"
TouchDown="DataTemplate_Touch"
MouseLeftButtonDown="DataTemplate_Click">
Then in your UserControl/window's code behind you can keep track of the selected item as such:
/// <summary>
/// MyScreen.xaml
/// </summary>
public partial class MyScreen : UserControl
{
private MyServiceWrapper _serviceWrapper;
public MyScreen()
{
InitializeComponent();
}
public MyScreen(MyServiceWrapper serviceWrapper)
{
//Instrumentation.Log(typeof(MyScreen), LogTypes.Trace, "Creating instance of MyScreen");
this._serviceWrapper = serviceWrapper;
// Set your DataContext, is this the class that would also have your
// CustomSalesProducts property exposed
this.DataContext = this._serviceWrapper;
InitializeComponent();
}
private void DataTemplate_Touch(object sender, System.Windows.Input.TouchEventArgs e)
{
SetSelectedCustomSalesProduct(sender);
}
private void DataTemplate_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SetSelectedCustomSalesProduct(sender);
}
private void SetSelectedCustomSalesProduct(object sender)
{
_serviceWrapper.SelectedCustomSalesProduct = ((Grid)sender).Tag as CustomSalesProduct;
}
}
I found that for using headers there is HeaderdItemsControl. With this I can add headers and also it is not repeatable. But problem with this is that we have to define static size for header and its item if we define auto size then the UI of headeredItemsControl is not perfect so we have to give its static size.
You can read this for how to use HeaderedItemsControl?

ListBox inside ListBox and selectedItem / Events

I have now been stuck at this for some time so I though to ask the experts here.
First the XAML:
<ListBox Grid.Column="0" Tap="Some_Tap" SelectionChanged="Some_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- the panel which covers a complete list item -->
<StackPanel Orientation="Vertical">
<!-- start and end time for the travel route-->
<Grid HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<!-- some textblock items, removed to keep this simple -->
</Grid>
<!-- list box for images -->
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Disabled" ItemsSource="{Binding Thumbs}" HorizontalAlignment="Center" Margin="0,10,0,50">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<Image Source="{Binding Image}" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Text}" HorizontalAlignment="Center" FontWeight="{Binding FontWeightForText}">
<TextBlock.Foreground>
<SolidColorBrush Color="{StaticResource PhoneForegroundColor}"/>
</TextBlock.Foreground>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
So as you can see from the XAML I have a ListBox inside the data template of another ListBox. I have Tap & SelectionChanged events wired.
Problem:
I can't click on the area covered by the inner list box, the tap or selection changed events for the outer list box are not fired for the outerlistbox.
I could wire tap and selection changed events for the inner listbox as well, but then how would I know which item in the outerlistbox this innerlistbox belongs to, selectedindex.
help ??
-A
If you don't need items selection in the list, use ItemsControl instead (for inner list in this situation)
Just set the inner ListBox control IsEnabled = false and the Tap events of the outer will start to execute!

Silverlight Listbox scrolling only works if i set the height

i have a listbox and i am trying to make the scrolling work without having to set the height, is this possible? Thanks. Below is the code. The scrolling does not work.
<ListBox Name="EmployeeListBox" Background="Transparent"
SelectionMode="Single"
ItemsSource="{Binding Employees, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate >
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="140" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" FontWeight="Bold" Text="Name:" />
<TextBlock x:Name="TextBlock1" Grid.Column="1" Grid.Row="0"
Text="{Binding Name}" />
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
and viewmodel:
public class EmployeeDataContext
{
public List<Employee> Employees { get; set; }
public EmployeeDataContext()
{
GetEmployeeList();
}
private void GetEmployeeList()
{
Employees = new List<Employee>();
for (int i = 0; i < 100; ++i)
{
Employees.Add(new Employee() { Name = "Gema Arterton" });
}
}
}
public class Employee
{
public string Name { get; set; }
}
(sorry for my bad english)
It depends too on what control is the listbox inside. If your listbox is inside a Grid, it should take all the space of that grid - if it is inside a stackpanel it will use the least amount os space possible so you will have to use fixed height/width. Try this:
<UserControl...>
<Grid>
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
</ListBox>
</Grid>
</UserControl/>
The listbox should use all screen size and show vertical scrollbars if there is enough items.
You can set the height in scrollviewer
<ScrollViewer VerticalScrollBarVisibility="Visible" Height="480">
<Grid Margin="0,0,0,50">
</Grid>
</ScrollViewer>

Resources