wpf.net-3.5convertersivalueconverterdatagridtextcolumn

Setting DataGridTextColumn Width


I have a MVVM WPF application.

I have a DataGridTextColumn in a WPF datagrid. I want to bind its width property to a converter and pass to it its cell value. For this column, there are cases where all cells for this column are empty so I also want to set the column width to a fixed value, 20 (the same as its MinWidth) in case all cells are empty, otherwise 50. The problem is that converter is not being called.

To simplify and focus on the interesting parts I only post here the relevant code:

 <DataGrid  Grid.Row="1"                          
               AutoGenerateColumns="False" 
               ItemsSource="{Binding Path=MyListOfItems}" 
               VerticalAlignment="Stretch" IsReadOnly="True" 
               SelectionMode="Single" ColumnWidth="*" 
               >

<DataGridTextColumn 
      CellStyle="{StaticResource MyDataGridCellStyle}"
      Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}" 
      Header="Entry Date" 
      Width="{Binding Path=EntryDate, Converter={StaticResource ColumnWidthConverter}}"
      HeaderStyle="{DynamicResource CenterGridHeaderStyle}">

</DataGridTextColumn> 

 </DataGrid>

Converter:

public class ColumnWidthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string cellContent= (string)value;

        return (string.IsNullOrEmpty(cellContent.Trim()) ? 20 : 50);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

My final goal is to set column width to 20 when all its cells are empty, otherwise set its width to 50. I thought that using a converter it will be a good idea but converter is never called. Why?

UPDATE: Finllay I have done what @Andy suggests: bind a property from view model to datagridtextcolumn width property on view. This property on view model iterates over all column cells, and then set the width accordingly. See below. My problem is that this property 'EntryDateColumnWidth' on view model only fires first time when application is launched, then when calling OnPropertyChanged("EntryDateColumnWidth"), it is not raised.

View model:

public class MyMainViewModel : ViewModelBase
{
  public DataGridLength EntryDateColumnWidth
  {
      get
      {
          bool isEmpty = this.MyListOfItems.TrueForAll(i => string.IsNullOrEmpty(i.EntryDate.ToString().Trim()));

          return (isEmpty ? 20 : new DataGridLength(0, DataGridLengthUnitType.Auto));
      }
  }
}

Also, from view model, when I have set the list of items for the datagrid, I perform:

OnPropertyChanged("EntryDateColumnWidth");

This property returns a DataGridLength object because I need to set width to auto when any of the column cells is not empty.

Note: ViewModelBase is an abstract class that implements INotifyPropertyChanged.

View:

<DataGrid  Grid.Row="1"                          
           AutoGenerateColumns="False" 
           ItemsSource="{Binding Path=MyListOfItems}" 
           VerticalAlignment="Stretch" IsReadOnly="True" 
           SelectionMode="Single" ColumnWidth="*">

<DataGrid.Resources>
   <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

<DataGridTextColumn 
      CellStyle="{StaticResource MyDataGridCellStyle}"
      Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}" 
      Header="Entry Date" 
      Width="{Binding Data.EntryDateColumnWidth, Source={StaticResource proxy}}"
      HeaderStyle="{DynamicResource CenterGridHeaderStyle}">

</DataGridTextColumn> 

</DataGrid>

Class BindingProxy:

namespace MyApp.Classes
{
    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable

        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }
}

UPDATE 2:

Dependency object class:

namespace My.WPF.App.Classes
{
    public class BridgeDO: DependencyObject
    {
        public DataGridLength DataComandaColWidth
        {
            get { return (DataGridLength)GetValue(DataComandaColWidthProperty); }
            set { SetValue(DataComandaColWidthProperty, value); }
        }

        public static readonly DependencyProperty EntryDateColWidthProperty =
            DependencyProperty.Register("EntryDateColWidth", 
                                        typeof(DataGridLength), 
                                        typeof(BridgeDO),                                         
                                        new PropertyMetadata(new DataGridLength(1, DataGridLengthUnitType.Auto)));
    }
}

Instance in the resource dictionary (DictionaryDO.xaml):

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:My.WPF.App.Classes">
    <local:BridgeDO x:Key="DO"/>
</ResourceDictionary>

Merging it into resource dictionary (app.xaml) :

<Application x:Class="My.WPF.Apps.MyApp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit"
    xmlns:local="clr-namespace:My.WPF.Apps.MyApp"
    StartupUri="Main.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionaries/DictionaryDO.xaml"/>
            </ResourceDictionary.MergedDictionaries>

            <!-- Styles -->
        </ResourceDictionary>
    </Application.Resources>
</Application>

Window :

<Window x:Name="MainWindow" x:Class="My.WPF.Apps.MyApp.wMain"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Window.Resources>
   <!-- Resources -->
</Window.Resources>

<DataGrid  Grid.Row="1"                          
           AutoGenerateColumns="False" 
           ItemsSource="{Binding Path=MyListOfItems}" 
           VerticalAlignment="Stretch" IsReadOnly="True" 
           SelectionMode="Single" ColumnWidth="*">

<DataGrid.Resources>
   <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

<DataGridTextColumn 
      CellStyle="{StaticResource MyDataGridCellStyle}"
      Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}" 
      Header="Entry Date" 
      Width="{Binding EntryDateColWidth, Source={StaticResource DO}}"
      HeaderStyle="{DynamicResource CenterGridHeaderStyle}">

</DataGridTextColumn> 

</DataGrid>

</Window>

View model:

public class myMainViewModel : ViewModelBase 
{
   private BridgeDO _do;
   public myMainViewModel(IView view)
   {
      _view = view;
      _do = Application.Current.Resources["DO"] as BridgeDO;            
   }


   private void BackgroundWorker_DoWork()
   {
      // Do some stuff
      SetColumnWidth();
   }


   private void SetColumnWidth()
   {
      _view.GetWindow().Dispatcher.Invoke(new Action(delegate
       {
          bool isEmpty = this.MyListOfItems.TrueForAll(e => !e.EntryDate.HasValue);
          _do.SetValue(BridgeDO.EntryDateColWidthProperty, isEmpty ? new DataGridLength(22.0) : new DataGridLength(1, DataGridLengthUnitType.Auto));

            }), DispatcherPriority.Render);
   }
}

But column width is not being updated...


Solution

  • OK, this demonstrates the principle of what I'm describing and it's a bit quick n dirty.
    Define a dependency object as a new class.

    using System.Windows;
    using System.Windows.Controls;
    
    namespace wpf_12
    {
        public class BridgeDO : DependencyObject
        {
            public DataGridLength ColWidth
            {
                get { return (DataGridLength)GetValue(ColWidthProperty); }
                set { SetValue(ColWidthProperty, value); }
            }
            public static readonly DependencyProperty ColWidthProperty =
                DependencyProperty.Register("ColWidth", typeof(DataGridLength), typeof(BridgeDO), new PropertyMetadata(new DataGridLength(20.0)));
        }
    }
    

    Create an instance in a resource dictionary.

    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:local="clr-namespace:wpf_12">
        <local:BridgeDO x:Key="DO"/>
    </ResourceDictionary>
    

    Merge that resource dictionary in app.xaml:

    <Application x:Class="wpf_12.App"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:local="clr-namespace:wpf_12"
                 StartupUri="MainWindow.xaml">
        <Application.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="Dictionary1.xaml"/>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </Application.Resources>
    </Application>
    

    Quick and dirty viewmodel, this will change the column width to auto 6 seconds after it's instantiated.

    using System.Collections.ObjectModel;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace wpf_12
    {
        public class MainWIndowViewModel
        {
            public ObservableCollection<object> Items { get; set; } = new ObservableCollection<object>
            {   new { Name="Billy Bob", ID=1},
                new { Name="Peter Parker", ID=2},
                new { Name="Sherlock Holmes", ID=2}
            };
    
            public MainWIndowViewModel()
            {
                ChangeWidth();
            }
            private async void ChangeWidth()
            {
                await Task.Delay(6000);
                var _do = Application.Current.Resources["DO"] as BridgeDO;
                _do.SetCurrentValue(BridgeDO.ColWidthProperty, new DataGridLength(1, DataGridLengthUnitType.Auto));
            }
        }
    }
    

    Use that in my window:

            Name="Window"
        >
        <Window.DataContext>
            <local:MainWIndowViewModel/>
        </Window.DataContext>
    
        <Window.Resources>
    
        </Window.Resources>
        <Grid>
            <DataGrid ItemsSource="{Binding Items}"
                      AutoGenerateColumns="False">
                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding Name}" Width="{Binding ColWidth, Source={StaticResource DO}}"/>
                    <DataGridTextColumn Binding="{Binding ID}"/>
                </DataGrid.Columns>
            </DataGrid>
        </Grid>
    </Window>  
    

    When I run this I start off with a narrow-ish column. Sit there watching it for a while and it changes to auto width and widens.