Automatically resize columns of wpf GridView - c#

I've gone through a few posts with similar requirements but haven't found a fully working solution yet.
I have a GridView in a ListView with a few columns
<ListView x:Name="TheList" DockPanel.Dock="Top"
ItemsSource="{Binding Itemlist}"
SelectionMode="Extended">
<ListView.View>
<GridView>
....
<GridViewColumn x:Name="valueColumn" Header="Value" DisplayMemberBinding="{Binding ItemValue}" Width="150"/>
</GridView>
</ListView.View>
</ListView>
I set the model and sub to the model's property changing
_viewModel = new MyModel();
DataContext = _viewModel;
_viewModel.ItemValueChanged += RunResizeLogic; //ItemValueChanged gets emitted when the property changes
And then based on some solutions I found I attempt resizing the column
private void RunResizeLogic(object sender, EventArgs e)
{
ResizeGridViewColumn(valueColumn);
}
private void ResizeGridViewColumn(GridViewColumn col)
{
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
if (double.IsNaN(col.Width))
{
col.Width = col.ActualWidth;
}
col.Width = double.NaN;
}));
}
My resize logic gets called when the my ItemValue changes, however, the column is only resizing when ItemValue changes again.
So, if it's current size is 5. the ItemValue size changes to 10, my logic runs with ActualWidth==5, column remains the same size (5). The ItemValue size changes again to 2, logic runs ActualWidth==2 and the column resizes to 10.
What am I doing wrong? Thanks.
EDIT:
Some of the posts I've looked through
GridViewColumn Width Adjustment
WPF: GridViewColumn resize event

Related

GridViewColumn autosize only work once

I am storing column widths on application exit and restore them on startup. Everything works fine unless user double click header. This would cause column width become double.NaN which I understood is a flag for autosizing. Then I have problems.
While investigating the issue I noticed what setting column width to NaN will enable auto-resizing but only for one time.
Here is a repro:
<ListView x:Name="listView">
<ListView.View>
<GridView>
<GridViewColumn Header="A" Width="NaN" />
</GridView>
</ListView.View>
</ListView>
Then add two buttons with following click handlers:
void button1_Click(object sender, RoutedEventArgs e) => listView.Items.Add("abcd");
void button2_Click(object sender, RoutedEventArgs e) => listView.Items.Add("ABCDEFGHIJKL");
Clicking button1 first will autosize column to fit "abcd". Clicking then button2 won't.
Why? Is there a workaround to have it either always autosizing or to at least disable user double-click resizing (tried this solution without success)?
You need to reset the Width of the column on each update:
void button2_Click(object sender, RoutedEventArgs e)
{
listView.Items.Add("ABCDEFGHIJKL");
GridView gv = listView.View as GridView;
gv.Columns[0].Width = gv.Columns[0].ActualWidth;
gv.Columns[0].Width = double.NaN;
}
And to disable double-click resizing you could handle the PreviewMouseLeftButtonDown event for the GridViewColumnHeader like this:
<ListView x:Name="listView">
<ListView.View>
<GridView>
<GridView.ColumnHeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="listView_PreviewMouseLeftButtonDown"/>
</Style>
</GridView.ColumnHeaderContainerStyle>
<GridViewColumn Header="A" Width="Auto" />
</GridView>
</ListView.View>
</ListView>
private void listView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) =>
e.Handled = e.ClickCount == 2;
This is a well know behaviour of the WPF GridView.
A generic solution for multiple columns is to register an event handler (my personal suggestion is for SizeChanged)
<ListView x:Name="listView" SizeChanged="listView_SizeChanged">
to do the update
private void listView_SizeChanged(object sender, SizeChangedEventArgs e)
{
foreach (GridViewColumn c in ((GridView)listView.View).Columns)
{
if (double.IsNaN(c.Width))
{
c.Width = c.ActualWidth;
}
c.Width = double.NaN;
}
}

How to rebind my View to a new data context on visibility change of window

I have a WPF Window which is bind to viewmodel as data context.
In view
In view I have a listview and in its view a gridview in which i have some static columns
<ListView ItemsSource="{Binding Variables}" Margin="0,76,0,0">
<ListView.View>
<GridView attachedBehaviors:GridViewColumnsBehavior.BindableColumns="{Binding PackagesColumns}">
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"></GridViewColumn>
<GridViewColumn Header="URI" DisplayMemberBinding="{Binding URI}"></GridViewColumn>
<GridViewColumn Header="ABC" DisplayMemberBinding="{Binding ReferencedPackages}"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
In view model i have bound dynamic column to gridview using observable collection
To generate dynamic columns i have used depenency property
public static readonly System.Windows.DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached(
"BindableColumns",
typeof(ObservableCollection<GridViewColumn>),
typeof(GridViewColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
private static void BindableColumnsPropertyChanged(
DependencyObject source, DependencyPropertyChangedEventArgs e)
{
GridView dataGrid = source as GridView;
var columns = e.NewValue as ObservableCollection<GridViewColumn>;
if (dataGrid != null)
{
// dataGrid.Columns.Clear();
if (columns == null)
{
return;
}
foreach (GridViewColumn column in columns)
{
dataGrid.Columns.Add(column);
}
On click of a button1 i hide the view. Now on click of button2 the viewmodel is again visible and I have to give a new object of viewmodel as datacontext but that doesn't updates the view with the new columns that i have binded using dependency property.
So is it possible to update content the view with new ViewModel on visiblity change?

Draw resizable table in WPF

I mean how should I display my datastructure for I can work with it like with table?
I want to have a table with rows and columns can be dynamicaly added and removed, but in the rest is should looks like a table.
Now it's represented like IList>, because I should resize it, as I said before. But now i want to display it in DataGrid, and be able to have rows, coulmns, and work with "Cells". But i cannot bind it properly, it it does not display anything, only an empty cell for every row.
What should I do? Or mby use arrays and resize them after each row/column addition?
Please, advice.
Now I have this one:
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
var storage = new Storage();
storage.Data.Add(new DataRow("Bla-bla")
{
new DataEntity() {Text = "bla-bla", Forces = new[] {ForceEnum.AA}},
new DataEntity() {Text = "bla-bla", Forces = new[] {ForceEnum.UA}}
});
DataListView.DataContext = new StorageModel(storage);
}
public class StorageModel
{
public StorageModel()
{
}
public StorageModel(IStorage storage)
{
DataRowList = new ObservableCollection<DataRow>(storage.Data);
}
public ObservableCollection<DataRow> DataRowList
{
get;
set;
}
}
public class DataRow : IList<DataEntity>
{
public string Name { get; private set; }
private readonly List<DataEntity> _list = new List<DataEntity>();
...
_
<ListView ItemsSource="{Binding DataRowList}" Name="DataListView">
<ListView.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
i want be able to create something similar to this, but with 2-way binding...
You seem to be asking for TreeView and not ListView since you have a tree alike data structure.
If you do not want to use a TreeView I would suggest you to use a simple ListView with a small trick. That is you could explose an Expander which will add or remove items underneath parent with an indent to fake tree look and still all cells in column would be resized together.
Btw dont forgot to define columns
Here is how:
<ListView Margin="10" Name="lvUsers">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
</GridView>
</ListView.View>
</ListView>
If you wish to implement that expander trick behavior I would suggest you to listen to event OnExpanded and add or remove the needed rows.

DataGrid column width doesn't auto-update

<DataGridTextColumn Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Binding="{Binding Change}" Width="Auto"/>
When the value of Change updates, its column doesn't update to fit the new value. So the column stays too small and the value is clipped.
Any ideas?
The DataGrid will increase column sizes to fit as the data becomes longer, but it does not automatically decrease column sizes when the length of the data decreases. In your example, you're right aligning the 'Change' column, and using the rest of the space for the 'Name' column.
Now, when a 'Change' property grows large enough that it should increase the column's width, the 'Name' column is refusing to shrink to accommodate, so you have to force a refresh yourself.
The following steps should do this for you (I've included a sample app to demo):
1) In your DataGridTextColumn Bindings (all except your * sized column) set NotifyTargetUpdated=True.
2) On your DataGrid, add a handler to the TargetUpdated event.
3) In your TargetUpdated event handler:
-- a) Set the DataGrid's * sized column's width to 0.
-- b) Call the UpdateLayout() method on the DataGrid.
-- c) Set the DataGrid's * sized column's width back to new DataGridLength(1, DataGridLengthUnitType.Star)
Example XAML:
<Window x:Class="DataGridTest.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>
<CollectionViewSource x:Key="MyObjectCollection" />
</Window.Resources>
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Click to Make Item 1s Text Longer" Click="Button_Click" />
<Grid>
<DataGrid x:Name="dg" ItemsSource="{Binding Source={StaticResource MyObjectCollection}}" AutoGenerateColumns="False" TargetUpdated="dg_TargetUpdated">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding First}" Width="1*"/>
<DataGridTextColumn Binding="{Binding Last, NotifyOnTargetUpdated=True}" Width="Auto" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</DockPanel>
</Window>
Example Code Behind:
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.ComponentModel;
namespace DataGridTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ObservableCollection<MyObject> myObjectList = new ObservableCollection<MyObject>();
public MainWindow()
{
InitializeComponent();
(this.FindResource("MyObjectCollection") as CollectionViewSource).Source = this.myObjectList;
this.myObjectList.Add(new MyObject() { First = "Bob", Last = "Jones" });
this.myObjectList.Add(new MyObject() { First = "Jane", Last = "Doe" });
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.myObjectList[0].Last = "BillyOBrian";
}
private void dg_TargetUpdated(object sender, DataTransferEventArgs e)
{
dg.Columns[0].Width = 0;
dg.UpdateLayout();
dg.Columns[0].Width = new DataGridLength(1, DataGridLengthUnitType.Star);
}
}
public class MyObject : INotifyPropertyChanged
{
private string firstName;
public string First
{
get { return this.firstName; }
set
{
if (this.firstName != value)
{
this.firstName = value;
NotifyPropertyChanged("First");
}
}
}
private string lastName;
public string Last
{
get { return this.lastName; }
set
{
if (this.lastName != value)
{
this.lastName = value;
NotifyPropertyChanged("Last");
}
}
}
public MyObject() { }
#region -- INotifyPropertyChanged Contract --
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion INotifyPropertyChanged Contract
}
}
i have had similar problem with my listview, the solution i found on how-to-autosize-and-right-align-gridviewcolumn-data-in-wpf here on stackoverflow.
In my case it was adding this piece of code into collectionchanged event handler of the observable collection the list view was bound to:
void listview_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
// this is a listview control
GridView view = this.View as GridView;
foreach(GridViewColumn c in view.Columns) {
if(double.IsNaN(c.Width)) {
c.Width = c.ActualWidth;
}
c.Width = double.NaN;
}
}
It works for me, although sometimes the user can notice "blink" over the columns.
WPF will just resize a datagrid's column width set to Auto if needed, i.e: the content cannot be displayed entirely. So when the content's width shrinks, the column does not resize as the content can still been seen entirely.
the only way I can see to force wpf to recalculate the columns' widths would be to force them all to 0 and then back to auto in the code behind, with one or two updateLayout() thrown in, but this is not very nice programming :-/
basically, in your code behind:
foreach (DataGridColumn c in dg.Columns)
c.Width = 0;
// Update your DG's source here
foreach (DataGridColumn c in dg.Columns)
c.Width = DataGridLength.Auto;
and you probably need a dg.UpdateLayout() or two somewhere in there (after the update and the setting back to auto probably)
One way you could solve this is by defining the width property of the column in a style setting and binding that setting to a property of the object you are binding to.
<DataGridTextColumn Binding="{Binding Change}" ElementStyle="{StaticResource ChangeColumnStyle}"/>
In your ResourceDictionary:
<Style TargetType="{x:Type DataGridTextColumn }" x:Key="ChangeColumnStyle">
<Setter Property="Width" Value="{Binding ColumnWidth}"
</Style>
ColumnWidth should be a property of your object. Now if you update this property from the setter of your 'Change' property (by using some self-defined algorithm, taking stuff like font into account), and calling:
RaisePropertyChanged("ColumnWidth");
It should update your column width.
public int Change
{
get { return m_change; }
set
{
if (m_change != value)
{
m_change = value;
ColumnWidth = WidthAlgo(numberOfCharacters);
RaisePropertyChanged("Change");
RaisePropertyChanged("ColumnWidth");
}
}
}
Have you tried this?
<DataGridTextColumn Binding="{Binding Path= Id}" Header="ID" IsReadOnly="True" Width="1*" />

WPF: GridViewColumn resize event

I'm using ListView with GridView. Is there GridViewColumn resize event?
I will handle the PropertyChanged event instead. The PropertyChanged event is not seen in the Visual Studio intellisense, but you can trick it :)
GridViewColumn column = ...
((System.ComponentModel.INotifyPropertyChanged)column).PropertyChanged += (sender, e) =>
{
if (e.PropertyName == "ActualWidth")
{
//do something here...
}
};
Although GridViewColumn does not appear to have a Resize event, you can bind to the ColumnWidth property.
You can verify this with sample XAML below - no code behind needed for this example. It binds only in one direction, from the column width to the text box, and when you resize you will see the textbox immediately update with the column width.
(This is just a simple example; if you want to pick up the resize in code I would create a class with a Width property so binding will work in both directions).
<StackPanel>
<ListView>
<ListView.View>
<GridView>
<GridViewColumn Width="{Binding ElementName=tbWidth1, Path=Text, Mode=OneWayToSource}" />
<GridViewColumn Width="{Binding ElementName=tbWidth2, Path=Text, Mode=OneWayToSource}" />
</GridView>
</ListView.View>
<ListViewItem>Item 1</ListViewItem>
<ListViewItem>Item 2</ListViewItem>
</ListView>
<TextBox Name="tbWidth1" />
<TextBox Name="tbWidth2" />
</StackPanel>
Have a look at MSDN DridViewColumn details. It does not appaer to have such an event, probably some workaround required, I am not sure though. have look here
Hope it helps.
private void ListView_Loaded( object sender, RoutedEventArgs e )
{
// Add the handler to know when resizing a column is done
((ListView)sender).AddHandler( Thumb.DragCompletedEvent, new DragCompletedEventHandler( ListViewHeader_DragCompleted ), true );
}
private void ListViewHeader_DragCompleted( object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e )
{
ListView lv = sender as ListView;
... code handing the resize goes here ...
}
XAML:
<ListView Loaded="ListView_Loaded">
Another approach: you can attach a change event handler to the GridViewColumn Width property:
PropertyDescriptor pd = DependencyPropertyDescriptor.FromProperty(
GridViewColumn.WidthProperty, typeof(GridViewColumn));
GridView gv = (GridView)myListView.View;
foreach (GridViewColumn col in gv.Columns) {
pd.AddValueChanged(col, ColumnWidthChanged);
}
...
private void ColumnWidthChanged(object sender, EventArgs e) { ... }
(Inspired by an answer here, for a similar question about DataGrid.)

Categories