1

In WPF, using the MVVM model, I am struggling with what I believe is a binding issue. I have a ListView designed to display a collection of objects. The ItemsSource is bound to the collection and each GridViewColumn to seperate properties of that object. This view (a UserControl) is generic across several tabs in a TabControl on the master window. Each tab requires certain columns to be hidden (logic happens in the ViewModel).

I have created a Behaviors class to attach DependecyProperties for several things, including a property IsHidden for the GridViewColumn. With this in mind, here's an example setup:

Behavior Classes - minimized examples

public static class LayoutColumn
{
  public static readonly DependencyProperty HiddenProperty = DependencyProperty.RegisterAttached(
      "IsHidden",
      typeof(bool),
      typeof(LayoutColumn));

  public static bool GetIsHidden(DependencyObject obj)
  {
    return (bool)obj.GetValue(HiddenProperty);
  }

  public static void SetIsHidden(DependencyObject obj, bool isHidden)
  {
    obj.SetValue(HiddenProperty, isHidden);
  }

  public static bool IsHidden(this GridViewColumn column)
  {
    bool? isHidden = column.GetProperty<bool>(HiddenProperty);

    // Debug
    string format = "{0}.IsHidden = {1}";
    if (isHidden.HasValue)
    {
      Console.WriteLine(format, column.Header, isHidden.Value);
    }
    else
    {
      Console.WriteLine(format, column.Header, "Null");
    }

    return isHidden.HasValue && isHidden.Value;
  }

  private static T? GetProperty<T>(this GridViewColumn column, DependencyProperty dp) where T : struct
  {
    if (column == null)
    {
      throw new ArgumentNullException("column");
    }

    object value = column.ReadLocalValue(dp);

    if (value != null && value.GetType() == dp.PropertyType)
    {
      return (T)value;
    }

    return null;
  }
} // end LayoutColumn class

public class LayoutManager
{
  // Methods and logic to enforce column min/max width, hidden, fixed, etc by attaching to the ListView and GridViewColumn event handlers.
}

An example object class

public class Example
{
  public string Foo { get; set; }
  public string Bar { get; set; }
  public string Baz { get; set; }
}

ViewModel

public class MyDisplayViewModel : INotifyPropertyChanged
{
  private bool hidden;
  private ObservableCollection<Example> examples;

  ...

  public bool HideColumn
  {
    get 
    {
      return this.hidden;
    }

    set 
    {
      this.hidden = value;
      this.OnPropertyChanged("HideColumn");
    }
  }

  public ObservableCollection<Example> ExampleCollection
  {
    get 
    {
      return this.examples;
    }

    set 
    {
      this.examples = value;
      this.OnPropertyChanged("ExampleCollection");
    }
  }
}

Some XAML

<ListView
        Name="LogListView"
        ItemsSource="{Binding ExampleCollection}"
        ListViewBehaviors:LayoutManager.Enabled="{Binding AttachProperty}">
        <ListView.View>
            <GridView>
                <GridViewColumn
                    Width="Auto"
                    ListViewBehaviors:LayoutColumn.MinWidth="40"
                    ListViewBehaviors:LayoutColumn.MaxWidth="200"
                    ListViewBehaviors:LayoutColumn.IsHidden="True"
                    DisplayMemberBinding="{Binding Foo, Mode=OneWay}"
                    Header="Hidden Column"/>
                <GridViewColumn
                    Width="Auto"
                    ListViewBehaviors:LayoutColumn.MinWidth="74"
                    ListViewBehaviors:LayoutColumn.MaxWidth="200"
                    ListViewBehaviors:LayoutColumn.IsHidden="False"
                    DisplayMemberBinding="{Binding Bar, Mode=OneWay}"
                    Header="Visible Column"/>
                <GridViewColumn
                    Width="Auto"
                    ListViewBehaviors:LayoutColumn.IsFixed="True"
                    ListViewBehaviors:LayoutColumn.IsHidden="{Binding Path=DataContext.HideColumn, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}},
                        Mode=OneWay}"
                    DisplayMemberBinding="{Binding Baz, Mode=OneWay}"
                    Header="Dynamic Visibility">
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>

Demonstrated here, manually setting the attached property IsHidden to true or false works correctly to hide/show the associated column. When IsHidden is null (the column doesn't have the attached behavior set) the expected behavior is for the column to show like normal.

Problem

The bound column always shows by default, which to me means IsHidden is not being set. I feel like I'm close, but all my research results in examples doing what I've done here. I'm fairly new to WPF, so I don't know what else to search for.

Edit

I've tried the following, but all failed:

AncestorType={x:Type ListView}
AncestorType={x:Type UserControl}
AncestorType={x:Type Window}

What do I need to do to bind the GridViewColumn attached property to the ViewModel HideColumn property?

OhBeWise
  • 5,350
  • 3
  • 32
  • 60
  • 1
    Please share your behaviour. Also your viewmodel. May be you need to set the RelativeSource to Window or Usercontrol – Ayyappan Subramanian Mar 05 '15 at 20:11
  • @Ganesh I edited in the ViewModel. I didn't add the behavior class since that would be a lot of bloat and I already know it works when manually setting true/false. Also tested for `UserControl` and `Window` but sadly both failed. – OhBeWise Mar 05 '15 at 20:45
  • Is the property IsHidden is DependencyProperty? – Ayyappan Subramanian Mar 05 '15 at 21:01
  • @Ganesh Yes. I'll post a minimal version of my behavior class if others think it would really help. – OhBeWise Mar 05 '15 at 21:04
  • 1
    Try this link. http://wickedflame-dev.blogspot.com/2012/05/seting-visibility-on-gridviewcolum-of.html. – Ayyappan Subramanian Mar 05 '15 at 22:39
  • 1
    Binding will work definitely (you can validate that by putting breakpoint on getter of HideColumn). Most likely logic of hiding doesn't work with binding in place. Can you post that code as well that how you are hiding the column? – Rohit Vats Mar 06 '15 at 02:10
  • @RohitVats The code hiding the column simply calls `bool? hidden = gridViewColumn.IsHidden();` Then if `hidden.HasValue && hidden.Value` is true, set the column width to 0 and disallow user interaction with the column header. I've tried breakpoints in `HideColumn` getter and setter and in `LayoutColumn.IsHidden`. The former two are hit when I set the dynamic column in the ViewModel. The latter is hit for each GridViewColumn, printing in order (the debug Console.WriteLine) *True*, *False*, *Null*. They seem bound, but the value isn't being set, else it would print *True*, *False*, *True*. – OhBeWise Mar 06 '15 at 15:44
  • 1
    It might be possible that binding is not resolved till that time when you check for IsHidden(). Can you upload the code sample somewhere over the cloud if possbile? – Rohit Vats Mar 06 '15 at 18:13
  • Thanks @Ganesh for the link. I struggled getting the answer from your link to work for me due to a lot of cancerous code from my previous attempts. But once that was clean, it works nicely. – OhBeWise Mar 11 '15 at 19:40
  • Also, thanks @RohitVats for pointing me in the direction of my logic. Though the logic was sound, the logic was never applied when I thought it should have been and your suggestion led me to questions that made me run tests and checks for things I wouldn't have otherwise checked. – OhBeWise Mar 11 '15 at 19:40

1 Answers1

0

As @RohitVats pointed out, the main error was in my assumption that the code to hide the column was playing nicely. I was relying on the LayoutManager to validate minimum and maximum widths - which (in LayoutColumn class) would return their values (adjusted to 0 if the column is hidden) and that in turn would resize the column. The idea works but requires a little more. Much of what I needed to fix the issue came from here as suggested by @Ganesh.

Cleaner XAML

Through lots of Console.WriteLine and more debugging, I found that I was over complicating the binding.

ListViewBehaviors:LayoutColumn.IsHidden="{Binding HideColumn}"

Additions to Behavior Class

The change in XAML was only able to work once I made some changes to the LayoutColumn class setup by adding PropertyMetadata to HiddenProperty and another DependencyProperty for saving the previous-to-hidden width:

public static readonly DependencyProperty HiddenProperty = DependencyProperty.RegisterAttached(
  "IsHidden",
  typeof(bool),
  typeof(LayoutColumn)
  new PropertyMetadata(false, OnHiddenChanged));

public static readonly DependencyProperty VisibleWidthProperty = DependencyProperty.RegisterAttached(
  "VisibleWidth",
  typeof(double),
  typeof(LayoutColumn),
  new UIPropertyMetadata(double.NaN));

public static double GetVisibleWidth(DependencyObject obj)
{
  return (double)obj.GetValue(VisibleWidthProperty);
}

public static void SetVisibleWidth(DependencyObject obj, double value)
{
  obj.SetValue(VisibleWidthProperty, value);
}

private static void OnHiddenChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
  GridViewColumn column = dependencyObject as GridViewColumn;

  if (column != null)
  {
    if (e.Property == HiddenProperty)
    {
      bool hide = (bool)e.NewValue;

      if (hide)
      {
        SetVisibleWidth(column, column.Width);
        column.Width = 0;
      }
      else
      {
        column.Width = GetVisibleWidth(column);
      }
    }
  }
}

And that's it! I didn't use a Converter as shown in the link, since I'm handling a bool type on both ends of the binding, but it works just as well. Hopefully this can be helpful to others.

OhBeWise
  • 5,350
  • 3
  • 32
  • 60