Welcome to the Xceed Community | Help
Community Search  
More Search Options

Tech Side

  • Merged Column Headers (New in DataGrid for WPF v4.5)

    You have read correctly, Merged Column Headers are finally here!

    Many have asked us this feature over the past 2+ years, and while it was not easy to implement, we have heard you and now your patience has finally paid off.

    First, let me start off with a little teaser; here is the kind of look that will now be supported built-in when using the new Merged Column Headers:

    Now I am sure that you are all impatient to find out exactly how to use them, so let’s jump right in!


    Part 1: Theory

    Here are the Classes that you will need to use to get the Merged Column Headers to work:

    At the UI level: MergedColumnManagerRow, MergedColumnManagerCell

    At the Data Level: MergedHeader, MergedHeaderCollection, MergedColumn, MergedColumnCollection


    Part 2: Practice

    I will use the example shown in the teaser screen shot for this tutorial. First, let’s start by visualizing this new look in a simple layout:

    As you can see, we have 2 MergedColumnManagerRows and then the ColumnManagerRow. This order is very important: the ColumnManagerRow will always be the last one at the bottom, and any MergedColumnManagerRow must be declared in the order they will appear on screen, from top to bottom.

      <xcdg:DataGridControl.View>

        <xcdg:TableflowView UseDefaultHeadersFooters="False">

          <xcdg:TableflowView.FixedHeaders>

            <DataTemplate>

              <!-- Quarters -->

              <xcdg:MergedColumnManagerRow />

            </DataTemplate>

            <DataTemplate>

              <!-- Months -->

              <xcdg:MergedColumnManagerRow />

            </DataTemplate>

            <DataTemplate>

              <xcdg:ColumnManagerRow />

            </DataTemplate>

          </xcdg:TableflowView.FixedHeaders>

        </xcdg:TableflowView>

      </xcdg:DataGridControl.View>

    Note: you only need to manually add the MergedColumnManagerRow(s) to the View if UseDefaultHeadersFooters is set to false (true by default). 


    In the XAML, we will need to add 1 MergedHeader for each MergedColumnManagerRow to be displayed. These are added to the DataGridControl.MergedHeaders collection.

    Then, in each MergedHeader, we need to specify the list of MergedColumns that will be present in it. A MergedColumn is made of a list of ChildColumnNames, which indicates which Columns or MergedColumns (of the level displayed directly under it) it regroups.

      <xcdg:DataGridControl.MergedHeaders>

        <xcdg:MergedHeader>

          <xcdg:MergedHeader.MergedColumns>

            <xcdg:MergedColumn FieldName="Q1" Title="Quarter 1"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="JAN,FEB,MAR" />

            <xcdg:MergedColumn FieldName="Q2" Title="Quarter 2"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="APR,MAY,JUN" />

          </xcdg:MergedHeader.MergedColumns>

        </xcdg:MergedHeader>

        <xcdg:MergedHeader>

          <xcdg:MergedHeader.MergedColumns>

            <xcdg:MergedColumn FieldName="JAN" Title="January"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="JanIn,JanOut" />

            <xcdg:MergedColumn FieldName="FEB" Title="February"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="FebIn,FebOut" />

            <xcdg:MergedColumn FieldName="MAR" Title="March"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="MarIn,MarOut" />

            <xcdg:MergedColumn FieldName="APR" Title="April"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="AprIn,AprOut" />

            <xcdg:MergedColumn FieldName="MAY" Title="May"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="MayIn,MayOut" />

            <xcdg:MergedColumn FieldName="JUN" Title="June"

                               CellHorizontalContentAlignment="Center"

                               ChildColumnNames="JunIn,JunOut" />

          </xcdg:MergedHeader.MergedColumns>

        </xcdg:MergedHeader>

      </xcdg:DataGridControl.MergedHeaders>


    Part 3: More

    This section will be used to list additional information, and may be edited in the future if needed.

    ColumnChooser: Each level has its own.

    Grouping and Sorting: Only available on ColumnManagerCells.

    FixedColumnSplitter: Only applied at the highest level, meaning at the top level MergedColumnManagerRow if any merged headers are present.

    Re-ordering: A Column or MergedColumn can be moved from one MergedHeader to another. They can also be re-ordered within the same MergedHeader. This can be done either by drag & drop, by changing the VisiblePosition, or by manually changing the group. To manually change the group, you can either use the MoveMergedColumn method on the DataGrid, or set its Parent to null (to detach it) and then assign the new Parent.

     

    If you have any questions or feedback, please send them to support@xceed.com.

    Good Luck and Have Fun!

  • It's All About the StatRows Part 2 - Code Behind

    Have you ever had that feeling that you couldn’t get enough of StatRows? Well if you haven’t had, then you will now. I’ve decided to write you a sequel on “It’s All About the StatRows” to bring you part 2 – Code Behind! I’ve noticed that there are more and more developers that are coding XAML in code behind. We all know how that feels because between bugs in .NET and the time spent doing this, it just does not seem to be a fun task. I wish this could be easier, but unfortunately it is not since you will need to store XAML in a string and then load it using XAMLReader.To avoid as much error as possible, code your XAML in XAML first so that you don't get any syntax errors when bringing into code behind. There won't be any beautiful red zig-zag lines in between quotations ("").

     

    So for the first topic of the post, I wanted to spend time showing you how to implement this in code behind. Before, we get started; I forgot to mention that I have noticed that this question was asked many times. I know that it was asked on StackOverflow and on our forums a bunch of times. I feel that writing a blog about this and hopefully those looking for answers will come here.

     

    Build it and they shall come!

     

    Let’s clear the air here and make everyone understand that, creating instances of DataTemplates, assigning styles and adding them to the Resources collection just won’t work in code behind. We have tried time and time again, until we found out that there is a bug in the .NET framework. You can attack this in any way and it just won’t cut. The only way to do it is to write the XAML and store it in a string. Here is a basic example:

     

    private DataTemplate GenerateStatRowDataTemplate()
    {
       ParserContext pc = new ParserContext();
       pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
       pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
       pc.XmlnsDictionary.Add("xcdg", "http://schemas.xceed.com/wpf/xaml/datagrid");

       string statRowTemplate = "<DataTemplate>";
       statRowTemplate+= "<xcdg:StatRow>";
       statRowTemplate += "<xcdg:StatCell FieldName=\"Column4\"   ResultPropertyName=\"AvgColumn4\">";
       statRowTemplate += "</xcdg:StatCell>";
       statRowTemplate += "</xcdg:StatRow>";
       statRowTemplate += "</DataTemplate>";

       StringReader stringReader = new StringReader(statRowTemplate);
       XmlReader xmlReader = XmlReader.Create(stringReader);
       MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(statRowTemplate.ToString()));
       

       DataTemplate dt = (DataTemplate)XamlReader.Load(ms,pc);
       dt.LoadContent();
       return dt;
    }

     

    This is the simplest way to achieve exactly what you want. The XamlReader.Load will load the XAML the way it should and place it in a DataTemplate instance. You can then add the DataTemplate into the FixedFooters or the groups footers. Voila! Now you have a StatRow which has now become dynamic.

     

    But what if we want to do more? Say, a CellContentTemplate for the StatCell? This is easy, but we still run into another issue. The ParserContext won’t recognize your local namespace. You can try, but you won’t succeed. So now what do we do? Our first step is to add the namespace into the DataTemplate in the XAML string variable. So let’s modify the above code.

     

    string statRowTemplate = "<DataTemplate xmlns:local=\"clr-namespace:TestProject;assembly=TestProject\">";

     

    That’s it!

     

    Now we have to give our converter a name. To do this, we must do it in the Resources of the DataTemplate. Here is the modified code:

     

    statRowTemplate += "<DataTemplate.Resources><local:StatCellConverter x:Key=\"myConverter\"/></DataTemplate.Resources>";

     

    That’s it again!

     

    And now we must code our DataTemplate so that we can provide it to the CellContentTemplate property of the StatCell. This is easy as well and does not require much effort. In the next piece of code, I had written a converter that took any values that were being divided by 0 and displaying an error message instead of “#DIV/0#”.

     

    statRowTemplate += "<xcdg:StatCell.ContentTemplate>";
    statRowTemplate += "<DataTemplate>";
    statRowTemplate += "<TextBlock Text=\"{Binding ., Converter={StaticResource ResourceKey=myConverter}}\" />";
    statRowTemplate += "</DataTemplate>";
    statRowTemplate += "</xcdg:StatCell.ContentTemplate>";

     

    So overall, this was not a difficult task at all. The only issue is that we must first recognize what works and what doesn’t. In an ideal world, we would want to do this in .NET code rather than storing XAML in code behind. But who said life was easy? So here is the final code up until now:

     

    private DataTemplate GenerateStatRowDataTemplate()
    {
       ParserContext pc = new ParserContext();
       pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
       pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
       pc.XmlnsDictionary.Add("xcdg", "http://schemas.xceed.com/wpf/xaml/datagrid");

       string statRowTemplate = "<DataTemplate xmlns:local=\"clr-namespace:TestProject;assembly=TestProject\">";
       statRowTemplate += "<DataTemplate.Resources><local:StatCellConverter x:Key=\"myConverter\"/></DataTemplate.Resources>";
       statRowTemplate+= "<xcdg:StatRow>";
       statRowTemplate += "<xcdg:StatCell FieldName=\"Column4\" ResultPropertyName=\"AvgColumn4\">";
       statRowTemplate += "<xcdg:StatCell.ContentTemplate>";
       statRowTemplate += "<DataTemplate>";
       statRowTemplate += "<TextBlock Text=\"{Binding ., Converter={StaticResource ResourceKey=myConverter}}\" />";
       statRowTemplate += "</DataTemplate>";
       statRowTemplate += "</xcdg:StatCell.ContentTemplate>";
       statRowTemplate += "</xcdg:StatCell>";
       statRowTemplate += "</xcdg:StatRow>";
       statRowTemplate += "</DataTemplate>";

       StringReader stringReader = new StringReader(statRowTemplate);
       XmlReader xmlReader = XmlReader.Create(stringReader);
       MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(statRowTemplate.ToString()));
       DataTemplate dt = (DataTemplate)XamlReader.Load(ms,pc);
       dt.LoadContent();
       return dt;
    }

     

    This does conclude the post on It’s All About the StatRows Part 2 – Code Behind. This does not mean that it is the end of StatRows blogging, but, for now I believe I have covered the more popular questions and concerns about it. I shall return shortly, and hopefully bring you more wisdom to carry forth your development adventures. Before I end it, I just want to say Happy New Year and welcome to 2012! Xceed is always looking to make improvements and to bring you the best components. Just remember that those of us on the Tech Side of things, we support you! Thanks for reading. Marc – out.

     

  • It's All About the StatRows

    Hey guys, in this topic, we will talk about StatRows. Yes that's right! I said StatRows. Although they are easy to implement, they can become tricky to play around with. In this lovely WPF world, we run into some unbelievable requests and features we need to implement with the DataGrid for WPF... I'm sure you all have other wonderful things to implement as well. From time to time, we get different questions about StatRows but there are always some that stick out like a sore thumb. And it is the sore thumbs that we want to fix since we well... don't want them to be sore again!

    For my first part about StatRows, I want to talk about adding statistical functions to the GroupHeaderControl. This has become a popular topic where developers wish to have StatCells displayed in the GroupHeaderControl rather than the footers of the groups. Although there will have to be a sacrifice to give up the original look of the GroupHeaderControl, it can be done. The one thing to remember is that we are adding a StatRow with StatCells in them. In order to get fast and efficient results, we need to use them. This helps developers avoid coding exhaustive loops in a Converter that will return a string which will then be bound to the Text property of a TextBlock which in turn will be part of the GroupHeaderControl template

    The first step would be to create a Style which targets the GroupHeaderControl. We will be changing the Template of the control to our own ControlTemplate which will be a Grid with a StatRow in there. So let's get the code ready so we can use it!

    <Style x:Key="groupHeaderAndStatRowStyle"

                    TargetType="{x:Type xcdg:GroupHeaderControl}">

     

                <!--Must set the GroupHeaderControl as scrollable horizontally -->

                <Setter Property="xcdg:TableView.CanScrollHorizontally"

                        Value="True" />

     

                <!-- Avoid transparency to allow Group Value clipping -->

                <Setter Property="Background"

                        Value="White" />

     

                <Setter Property="Template">

                   <Setter.Value>

                      <ControlTemplate TargetType="xcdg:GroupHeaderControl">

                         <Grid>

                            <xcdg:StatRow x:Name="statRow"

                                          Background="{TemplateBinding Background}">

     

                               <xcdg:StatCell FieldName="PreviousClose"

                                              ResultPropertyName="PreviousCloseAverage" />

                               <xcdg:StatCell FieldName="Open"

                                              ResultPropertyName="OpenAverage" />

                               <xcdg:StatCell FieldName="Change"

                                              ResultPropertyName="ChangeAverage" />

                               <xcdg:StatCell FieldName="ChangeDiff"

                                              ResultPropertyName="ChangeDiffAverage" />

                               <xcdg:StatCell FieldName="LastTrade"

                                              ResultPropertyName="LastTradeAverage" />

                               <xcdg:StatCell FieldName="LastTradeDiff"

                                              ResultPropertyName="LastTradeDiffAverage" />

                            </xcdg:StatRow>

     

                            <!-- The PassiveLayoutDecorator must not scroll horizontally -->

                            <!-- In the original GroupHeaderControl template, the GroupHeaderControl

                                 itself was not scrolling horizontally, now only its content should

                                 not scroll to allow the StatRow to correctly scroll -->

                            <xcdg:PassiveLayoutDecorator Axis="Horizontal"

                                                         xcdg:TableView.CanScrollHorizontally="False">

                               <DockPanel>

                                  <xcdg:HierarchicalGroupLevelIndicatorPane DockPanel.Dock="Left" />

                                  <xcdg:GroupLevelIndicatorPane DockPanel.Dock="Left"

                                                                Indented="False"

                                                                xcdg:GroupLevelIndicatorPane.GroupLevel="{Binding

                                                                RelativeSource={RelativeSource TemplatedParent},

                                                                Path=(xcdg:GroupLevelIndicatorPane.GroupLevel),

                                                                Converter={StaticResource

                                                                                groupHeaderControlGroupLevelConverter},

                                                                ConverterParameter=-1}" />

                                  <Border x:Name="mainBorder"

                                          BorderBrush="{TemplateBinding BorderBrush}"

                                          BorderThickness="{TemplateBinding BorderThickness}"

                                          Padding="{TemplateBinding Padding}"

                                          Focusable="True"

                                          FocusVisualStyle="{TemplateBinding FocusVisualStyle}">

                                     <Border.InputBindings>

                                        <KeyBinding Command="{x:Static xcdg:DataGridCommands.ToggleGroupExpansion}"

                                                    Key="Space" />

                                        <KeyBinding Command="{x:Static xcdg:DataGridCommands.ExpandGroup}"

                                                    Key="Right" />

                                        <KeyBinding Command="{x:Static xcdg:DataGridCommands.ExpandGroup}"

                                                    Key="Add" />

                                        <KeyBinding Command="{x:Static xcdg:DataGridCommands.CollapseGroup}"

                                                    Key="Left" />

                                        <KeyBinding Command="{x:Static xcdg:DataGridCommands.CollapseGroup}"

                                                    Key="Subtract" />

                                        <MouseBinding

                                                    Command="{x:Static xcdg:DataGridCommands.ToggleGroupExpansion}"

                                                    MouseAction="LeftDoubleClick" />

                                     </Border.InputBindings>

                                     <DockPanel LastChildFill="False">

                                        <ToggleButton DockPanel.Dock="Left"

                                                      OverridesDefaultStyle="True"

                                                      Template="{StaticResource groupExpanderToggleButtonTemplate}"

                                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                                      Focusable="False"

                                                      IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Group.IsExpanded}" />

                                        <!-- ContentPresenter in charge of displaying this GroupHeaderControl's Content, which is a Group by default. -->

     

                                        <!-- Replaced Margin by Padding to ensure the border is all over the

                                             StatRow and clips the displayed values correctly -->

                                        <Border Background="{TemplateBinding Background}"

                                                DockPanel.Dock="Left"

                                                Padding="3,0,0,0">

                                           <ContentPresenter Content="{TemplateBinding Content}"

                                                             ContentTemplate="{TemplateBinding ContentTemplate}"

                                                             ContentTemplateSelector="{TemplateBinding

                                                                   ContentTemplateSelector}"

                                                             VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                                             HorizontalAlignment="Left" />

                                        </Border>

                                     </DockPanel>

                                  </Border>

                               </DockPanel>

                            </xcdg:PassiveLayoutDecorator>

     

                         </Grid>

                         <ControlTemplate.Triggers>

                            <Trigger Property="xcdg:DataGridControl.NavigationBehavior"

                                     Value="None">

                               <Setter TargetName="mainBorder"

                                       Property="Focusable"

                                       Value="False" />

                            </Trigger>

                         </ControlTemplate.Triggers>

                      </ControlTemplate>

                   </Setter.Value>

                </Setter>

             </Style>

    I had just wanted to mention that along with the StatRows being in the ControlTemplate, we are simply redoing the ControlTemplate of the GroupHeaderControl. Jenny (those of you who don't know her, she writes the "Don't Panic" Blog on our website) had posted a sample application that shows you how to achieve this. Everything but the StatRow elements in the above XAML was just copy/pasted from our templates for the GroupHeaderControl.

    So far, this has been a great start where we now know how to add statistical functions to the GroupHeaderControl. So this brings me to my next and last topic (for this blog) about StatRows... Building custom statistical functions.

    One interesting and popular statistic that developers usually seek is to display the average for a Column which has a TimeSpan data type. This is just a scenario that I picked, which I found became popular, but again, you can use this for any scenario where you need an average. If the AverageFunction is not sufficient enough for you, possibly because you have a certain data type where you wish to calculate it in another way, you can certainly use this as a reference.

    The first thing we must do is create our own class, let's say ‘DurationAvgFunction' for simplicity. We must inherit from StatFunction and override the following members:

    -          void Reset()

    -          void Accumulate(object[] values) - leave blank since we will be using Prerequisites

    -          bool RequiresAccumulation - return False since we will be using Prerequisites

    -          StatFunctions[] PrerequisiteFunctions - ReadOnly

    -          void InitializePrerequisites(StatResult[] prerequisiteValues)

    -          StatResult GetResult()

    The PrerequisiteFunctions property is extremely important and which it will require us to return an array with a Length of 2. The array will contain the sum of the duration (using our very own DurationSumFunction) as well as the count (the number of items required to calculate the average). We will return the value of a StatFunction array so later on we can extract the two values (sum and count).

    protected override StatFunction[] PrerequisiteFunctions

    {

        get

        {

           StatFunction[] prerequisites = m_prerequisites;

     

           if (prerequisites == null)

           {

             prerequisites = new StatFunction[]

             {

               new DurationSumFunction( "DurationAvgFunction.Sum", this.SourcePropertyName ),

               new CountFunction( "DurationAvgFunction.Count", this.SourcePropertyName )

             };

     

             if (this.IsSealed)

             {

               m_prerequisites = prerequisites;

             }

           }

        return prerequisites;

        }

    }

    In the InitializePrerequisites method, we will just initialize 2 local variables which will hold the values of the sum and count function.

    protected  override  void InitializePrerequisites(StatResult[] prerequisiteValues)

    {

      if (prerequisiteValues.Length != 2)

      {

         throw new InvalidOperationException("The prerequisites initializers do not match the PrerequisiteFunctions");

      }

      else

      {

         durationSum = (TimeSpan)prerequisiteValues[0].Value;

         durationCount = (long)prerequisiteValues[1].Value;

      }

    }

    The next important piece is the GetResult function which will return the StatResult. All we need to do here is to return the Average (sum / count) as a StatResult.

    protected override StatResult GetResult()

    {

      TimeSpan durationAvg = new TimeSpan();

     

      if (durationCount != null && durationSum != null)

      {

         if ((long)durationCount > 0)

         {

            durationAvg = TimeSpan.FromSeconds(((TimeSpan)durationSum).TotalSeconds / (long)durationCount);

         }

      }

     

      return new StatResult(durationAvg);

    }

    As we always do, here are the links to the sample applications that I used to test the above code. Didn't think I could actually do this with my eyes closed did you?

    Prerequisite Functions
    StatRow in GroupHeaderControl

    I hope you enjoyed this one, but if you haven't, then I hope it helped you in some way. I will conclude this post by saying "thank you!" to all the readers and for your support! Marc - out.

  • Adding a "Select All" button the AutoFilterControl

    A question we've had quite a few times is how to add a “select-all” button to the AutoFilterControl along with the "Clear All" button to have something that mimics Excel's filtering. 

    The steps to do this are:

    • Create an explicit style that targets  AutoFilterControl and sets the Template property.
    • Change the template basing it on our default AutoFilterControl template found in the themes folder of the DataGridControl's installation folder. 
    • Add the “select-all” button and handle its Click event in order to add the custom logic that selects all the AutoFilterValues of that column by adding all the distinct values to the AutoFilterValues. 
    • Apply this style to all the columns by setting it to the AutoFilterControlStyle that property of the columns that you want to have the "select-all" button.  Note that if your collection has a larger number of distinct items, the “select-all” procedure might freeze the UI for a while until the AutoFilterValues are all populated. We can however increase the performance by using DeferINotifyCollectionChanged on the AutoFilterValues.

     

    This is the style:

    <Style x:Key="PART_AutoFilterControlStyle" TargetType="{x:Type xcdg:AutoFilterControl}">

           <Setter Property="Template">

             <Setter.Value>

               <ControlTemplate TargetType="{x:Type xcdg:AutoFilterControl}">

                 <Border Background="{TemplateBinding Background}"

                         BorderBrush="{TemplateBinding BorderBrush}"

                         BorderThickness="{TemplateBinding BorderThickness}">

                   <Grid>

                     <Grid.RowDefinitions>

                      <RowDefinition Height="Auto" />

                      <RowDefinition Height="Auto" />

                      <RowDefinition Height="*" />

                        </Grid.RowDefinitions>

                          <Button Content=“select-all”

                            Click="OnAutoFilterSelectClearAllClick"

                            Grid.Row="0"

                            MinHeight="24"

                            Tag="1"/>

                             <Button Content="Clear All"

                               Click="OnAutoFilterSelectClearAllClick"

                               Grid.Row="1"

                               MinHeight="24"

                               Tag="0"/>

                            <ListBox Name="PART_DistinctValuesHost"

                               Background="{TemplateBinding Background}"

                               BorderThickness="0"

                               Foreground="{TemplateBinding Foreground}"

                               Grid.Row="2"

                               ItemTemplate="{TemplateBinding DistinctValueItemTemplate}"

                               ItemTemplateSelector="{TemplateBinding DistinctValueItemTemplateSelector}"

                               ItemContainerStyle="{TemplateBinding DistinctValueItemContainerStyle}"

                               ItemContainerStyleSelector="{TemplateBinding DistinctValueItemContainerStyleSelector}"

                               MaxHeight="350"

                               SelectionMode="Multiple"

                               ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>

                         </Grid>

                      </Border>

                   </ControlTemplate>

                </Setter.Value>

             </Setter>

          </Style>

     

    Applying the style to all the columns:

        myGrid.ItemsSourceChangeCompleted += new EventHandler( myGrid_ItemsSourceChangeCompleted );

       

     

        void myGrid_ItemsSourceChangeCompleted( object sender, EventArgs e )

        {

     

          foreach( var col in myGrid.Columns )

     

            col.AutoFilterControlStyle = ( Style )FindResource( "PART_AutoFilterControlStyle" );      

        }

    Handling the click event of the “select-all” button:

     

    private void OnAutoFilterSelectClearAllClick( object sender, RoutedEventArgs e )

        {

     

          DataGridCollectionView dataGridCollectionView = ( DataGridCollectionView )myGrid.ItemsSource;

          if( dataGridCollectionView != null )

          {

            int filters = 0;

            System.Windows.Controls.Button button = ( System.Windows.Controls.Button )sender;

            AutoFilterControl autoFilterControl = button.TemplatedParent as AutoFilterControl;

            string columnFieldName = autoFilterControl.AutoFilterColumn.FieldName;

            bool selectAll = ( ( string )button.Tag ) == "1";

     

            using( dataGridCollectionView.DeferRefresh() )

            {

              ObservableHashList autoFilterValues = dataGridCollectionView.AutoFilterValues[ columnFieldName ] as ObservableHashList;

              using( autoFilterValues.DeferINotifyCollectionChanged() )

              {

                autoFilterValues.Clear();

                if( selectAll )

                {

     

                  IList distinctValues = dataGridCollectionView.DistinctValues[ columnFieldName ];

                  foreach( object value in distinctValues )

                    autoFilterValues.Add( value );

                  filters += distinctValues.Count;

                }

              }

     

              if( selectAll )

              {

                ListBox listBox = autoFilterControl.DistinctValuesHost as ListBox;

                listBox.SelectAll();

              }

            }

          }

        }

     

    And that’s about it! You can download a sample application HERE ! Party!!!

     

     

  • How To Add Excel Like Selection of DataCells In Xceed DataGridControl

    Recently, we had a question here at technical support asking if we had "Excel Like Selection of DataCells".

    Well... we don't! When SelectionUnit is set to Cell on DataGridControl it is possible to make your selection on a cell-by-cell basis and to select multiple cells at once by Shift Clicking but still, that is not what some clients expect, especially if they are used to an Excel-like selection. That is, clicking on one cell, holding down the mouse key and as you move the mouse around, the cells in the rectangle formed by the clicked cell and the current location of the mouse should dynamically become selected and the content should scroll as you move your mouse to the edges of the DataGridControl .

    But of course, nothing is impossible! This behavior can be achieved but it requires a little extra work.

    This post will show how to customize the DataGridControl in order to add this functionality. The demo application is provided below.

    First thing we need to do is to create a custom DataGridControl, in this post I will name my class ExelSelectionDataGrid  which inherits from DataGridControl.

    We will keep a reference to the first clicked cell and the cell that lies under the mouse as the mouse moves. We will then dynamically calculate and add the ranges of cells that lie in the rectangle bounded by the first clicked cell and the cell beneath the mouse to the SelectedCellRanges collection of the datagrid instance.

    For that, we would need to override the following:

    OnPreviewMouseLeftButtonDown In order to get the keep track of the first clicked cell

    OnPreviewMouseMove where we get the cell that is currently under the mouse, find the ranges between the first cell and the current cell and update the selected ranges, and finally scroll the DataGridControl if we are close to any of the edges if possible. 

    OnPreviewMouseLeftButtonUp only to set our selection flag to false.

    Getting the cell under the mouse can easily be acheived by using the VisualTreeHelperClass. This is how I used it:

    private DataCell getDataCellUnderMouse(MouseEventArgs e)

    {

         HitTestResult result = VisualTreeHelper.HitTest(this, e.MouseDevice.GetPosition(this));

         DependencyObject obj = result.VisualHit;

         var ctype = typeof(DataCell);

         if (obj != null)

         {

              var otype = obj.GetType();

             while (obj != null && (obj as DataCell) == null)

             {

             obj = VisualTreeHelper.GetParent(obj);

             }

         }

        return obj as DataCell;

    }

     Manually scrolling the datagrid would require access to the scroll viewer, this can be reached through the following:

    ScrollViewer  sv = (ScrollViewer)this.Template.FindName("PART_ScrollViewer", this);

    So this is the entire class: 

     

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using Xceed.Wpf.DataGrid;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Input;

    using System.Diagnostics;

    using System.Windows.Media;

    using Xceed.Wpf.DataGrid.Views;

     

    namespace ExcelLikeSelectionDataGrid

    {

     

        class ExelSelectionDataGrid : DataGridControl

        {

            public ExelSelectionDataGrid()

            {

                this.Loaded += new RoutedEventHandler(CustomDataGridControl_Loaded);

            }

     

            void CustomDataGridControl_Loaded(object sender, RoutedEventArgs e)

            {

                sv = (ScrollViewer)this.Template.FindName("PART_ScrollViewer", this);

            }

            ScrollViewer sv;

     

     

            int firstItemIndex;

            int secondItemIndex;

            int firstColumnIndex;

            int secondColumnIndex;

     

            protected override void OnPreviewMouseMove(MouseEventArgs e)

            {

                base.OnPreviewMouseMove(e);

     

                if (isSelecting)

                {

                    DataCell cellUnderMouse = getDataCellUnderMouse(e);

                    if (cellUnderMouse != null)

                    {

                        int columnIndex = this.Columns[cellUnderMouse.FieldName].VisiblePosition;

                        int itemIndex = this.Items.IndexOf(cellUnderMouse.DataContext);

                        if ((columnIndex != secondColumnIndex || itemIndex != secondItemIndex) && columnIndex >= 0 && itemIndex >= 0)

                        {

                            secondColumnIndex = columnIndex;

                            secondItemIndex = itemIndex;

                            UpdateSelection();

                        }

                    }

                    Point mousePosition = Mouse.GetPosition(this);

                 

                    if (mousePosition.Y > (this.ActualHeight - 10))

                    {

                        double calculatedVerticalOffset = sv.VerticalOffset + 3 * (this.View as TableflowView).ContainerHeight;

                        sv.ScrollToVerticalOffset(calculatedVerticalOffset);

                    }

                    if (mousePosition.X > (this.ActualWidth - 10))

                    {

                        double calculatedHorizontalOffset = sv.HorizontalOffset + 3 * 50.0;

                        sv.ScrollToHorizontalOffset(calculatedHorizontalOffset);

                    }

     

                    if (mousePosition.Y < (10))

                    {

                        double calculatedVerticalOffset = sv.VerticalOffset - 3 * (this.View as TableflowView).ContainerHeight;

                        sv.ScrollToVerticalOffset(calculatedVerticalOffset);

                    }

                    if (mousePosition.X < 10)

                    {

                        double calculatedHorizontalOffset = sv.HorizontalOffset - 3 * 50.0;

                        sv.ScrollToHorizontalOffset(calculatedHorizontalOffset);

                    }

                }

            }

            protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)

            {

                base.OnPreviewMouseRightButtonDown(e);

                DataCell clickedCell = getDataCellUnderMouse(e);

                if (clickedCell != null)

                {

                    isSelecting = true;

                    firstColumnIndex = this.Columns[clickedCell.FieldName].VisiblePosition;

                    firstItemIndex = this.Items.IndexOf(clickedCell.DataContext);

                }

            }

           

            protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)

            {

                isSelecting = false;

            }

     

     

            private void UpdateSelection()

            {

                if (!(Keyboard.Modifiers == ModifierKeys.Shift))

                    this.SelectedCellRanges.Clear();

     

                this.SelectedCellRanges.Add(new SelectionCellRange(firstItemIndex, firstColumnIndex, secondItemIndex, secondColumnIndex));

            }

            bool isSelecting = false;

          

     

         

     

            private DataCell getDataCellUnderMouse(MouseEventArgs e)

            {

                HitTestResult result = VisualTreeHelper.HitTest(this, e.MouseDevice.GetPosition(this));

                DependencyObject obj = result.VisualHit;

     

                var ctype = typeof(DataCell);

                if (obj != null)

                {

                    var otype = obj.GetType();

     

                    while (obj != null && (obj as DataCell) == null)

                    {

                        obj = VisualTreeHelper.GetParent(obj);

                    }

                }

                return obj as DataCell;

            }

        }

    } 

    I will not go through the code details here. It should be straightforward to understand once you look at it. You can download a sample application here.

    Please, add your comments and suggestions below. Let me know if you find a better way to acheive this or if you find any bugs in my code ;)

     

  • How to Lookup on Data Virtualization

    Hi! My name is Marc and I work in the technical support department here at Xceed. My main focus is to help those seeking help and give developers something exciting to read about. Since data virtualization is plenty to get excited about, let’s start there.

    We all try to understand exactly what is going on in the “Data Virtualization” world and try to grasp its intentions; however, we sometimes need to sit back and take a look at what our possibilities are. What if we can go beyond that and try to cheat a little (as long as no one gets hurt)? Being from technical support and noticing that the same questions are often asked. How can something that seems so trivial be so complicated? Usually getting selected items is easy to achieve by simply accessing a property of a control. If you have tried to access the SelectedItems property from the DataGridControl when using data virtualization, you now know that an exception will be thrown stating that the operation is not supported. This happens when the number of SelectedItems exceeds the PageSize because these records aren’t loaded into memory yet, and the ones that were, well, they’re gone anyways since I scrolled down.

    But not to worry, there are workarounds! This actually became a common question, especially when you want to allow deletion of multiple rows. So today I thought I would create this post in hopes of answering some of your questions.

    Like I mentionned before, I will go ahead and explain how to delete multiple rows, although this workaround can apply to various other scenarios.

    So to make things a little clearer, we will begin by creating a class called VirtualizationHelper. And from here on out, we will use this class to process any lookups that need to be done.

    public class VirtualizationHelper<T>
           where T : class
    {

        private static List<int> OrderSelectionRangesIndexes( IList<SelectionRange> selectedItemRanges )
        {
          List<int> indexes = new List<int>();
     

     

          foreachvar selectionRange in selectedItemRanges )
          {
            forint i = selectionRange.StartIndex; i <= selectionRange.EndIndex; i++ )
            {
              indexes.Add( i );
            }
          }
     

     

          return ( from i in indexes orderby i select i ).ToList<int>();
        }

     

        public VirtualizationHelper( DataGridControl dataGridControl )
        {
          iftypeof( T ) == typeofobject ) )
            throw new InvalidOperationException();

          m_dataGridControl = dataGridControl;
        }

        public void ProcessSelectionLookUp()
        {
          if( m_orderedSelectionIndexes != null )
            throw new InvalidOperationException();

          var indexes = VirtualizationHelper<T>.OrderSelectionRangesIndexes( m_dataGridControl.SelectedItemRanges );

          if( indexes.Count() == 0 )
            return;

          m_orderedSelectionIndexes = indexes;

          INotifyCollectionChanged notifyCollectionChanged = m_dataGridControl.Items as INotifyCollectionChanged;

          notifyCollectionChanged.CollectionChanged += ( sender, args ) =>
            {
              if( args.Action == NotifyCollectionChangedAction.Replace )
              {
                if( ( m_selectedIndexWaitingForRealizedItem != -1 ) && ( m_selectedIndexWaitingForRealizedItem == args.OldStartingIndex ) )
                {
                  m_selectedIndexWaitingForRealizedItem = -1;
     

                  m_dataGridControl.Dispatcher.BeginInvoke( new Action( () => this.ProcessSelectionLookUpCore() ) );
                }
              }
            };

          this.ProcessSelectionLookUpCore();
        }

        public event EventHandler<LookUpEventArgs<T>> SelectedItemLookUp;
        public event EventHandler SelectedItemLookUpCompleted;

        private void ProcessSelectionLookUpCore()
        {
          if( m_orderedSelectionIndexes == null )
            throw new InvalidOperationException();

          while( m_orderedSelectionIndexes.Count > 0 )
          {
            int globalIndex = m_orderedSelectionIndexes.Last();
            T item = m_dataGridControl.Items[ globalIndex ] as T;

            if( item == null )
            {
              // Item not yet realized.
              m_selectedIndexWaitingForRealizedItem = globalIndex;

              return;
            }

            // Item is realized
            this.OnSelectedItemLookUp( item, globalIndex );
            m_orderedSelectionIndexes.Remove( globalIndex );
          }

          this.OnSelectedItemLookUpCompleted();

          return;
        }

        private void OnSelectedItemLookUp( T item, int globalIndex )
        {
          ifthis.SelectedItemLookUp != null )
            this.SelectedItemLookUp( thisnew LookUpEventArgs<T>( item, globalIndex ) );
        }

        private void OnSelectedItemLookUpCompleted()
        {
          ifthis.SelectedItemLookUpCompleted != null )
            this.SelectedItemLookUpCompleted( this, EventArgs.Empty );

          // The whole lookup of all the selected items is completed.

          m_orderedSelectionIndexes = null;

          m_selectedIndexWaitingForRealizedItem = -1;

          var collectionView = m_dataGridControl.ItemsSource as ICollectionView;

          collectionView.Refresh();

          m_dataGridControl = null;
        }

        private List<int> m_orderedSelectionIndexes = null;
        private int m_selectedIndexWaitingForRealizedItem = -1;
        private DataGridControl m_dataGridControl;
      }

      public class LookUpEventArgs<T> : EventArgs
        where T : class
      {
        public LookUpEventArgs( T item, int globalIndex )
        {
          this.Item = item;
          this.GlobalIndex = globalIndex;
        }

        public T Item { getprivate set; }
        public int GlobalIndex { getprivate set; }
      }

    Now we can send in our DataGridControl and get the SelectedItems we need. Even though this does defeat the whole purpose of data virtualization, there are those who may want to do something with those records that are not in memory yet. Now back to business… So we have this class, but how do we use it? The next part is pretty simple since the helper class does all the work for you, but you still have to call the appropriate methods.

    In your code behind, handle the DeletingSelectedItems event and use the helper class from there.

    void dataGridControl1_DeletingSelectedItems( object sender, CancelRoutedEventArgs e )
    {
       e.Cancel = true;

       this.DeleteSelectedItems();

       var collectionView = dataGridControl1.ItemsSource as ICollectionView;

       collectionView.Refresh();

       dataGridControl1.IsEnabled = true;
    }

    private void DeleteSelectedItems()
    {
       dataGridControl1.IsEnabled = false;

       var helper = new VirtualizationHelper<Record>( dataGridControl1 );

       helper.SelectedItemLookUp += ( sender, args ) =>
       {
          myBusinessObjectCollection.Remove( args.Item.Index );
       };

       helper.SelectedItemLookUpCompleted += ( sender, args ) =>
       {
          dataGridControl1.SelectedItemRanges.Clear();
            dataGridControl1.IsEnabled = true;
       };
     

       helper.ProcessSelectionLookUp();
    }

    So once we have deleted whatever we had to, we need to refresh the CollectionView so that the DataGridControl can reflect the changes that have been made.

    One issue to keep in mind is that the above solution deletes when a user selects DataRows from top to bottom. If you are looking to implement a delete capability, please remember that you need to handle selection as well as selecting DataRows from bottom to top (user clicks on the 100th record and holds the Shift key and clicks on the 25th record). If it is not handled, the DataGrid won’t delete those items. It is important to think of everything that the user can do.

    I will conclude this post by saying "thank you!" to all the readers and for your support! Marc - out.

  • DataCell Styling vs CellContentTemplate

    For this blog post from The Tech Side, I will be addressing a few queries that are very common when it comes to styling the DataCells and changing a Column's CellContentTemplate.

    I will demonstrate this by answering the following question: If I have 2 cells, how do I color their backgrounds depending on their values?

    Using the following criteria, this was my first try:

    If A < B then B's background is GREEN and A's background is RED
    If A > B then A's background is GREEN and B's background is RED

    <Style TargetType="{x:Type xcdg:DataCell}">

       <Style.Triggers >

          <MultiDataTrigger>

             <MultiDataTrigger.Conditions>

                <Condition Binding="{Binding RelativeSource={RelativeSource self}, 
                                                               
    Path
    =ParentColumn.FieldName}"
                                 
    Value="FieldA"/>

                <Condition Binding="{Binding Converter={StaticResource compareColorConverter},
                                                                
    RelativeSource={RelativeSource self}, 
                                                                
    Path
    =ParentRow.DataContext}" 
                                  
    Value
    ="more"/>

             </MultiDataTrigger.Conditions>

             <Setter Property="Background" Value="Green" />

          </MultiDataTrigger>

          <MultiDataTrigger>

             <MultiDataTrigger.Conditions>

                <Condition Binding="{Binding RelativeSource={RelativeSource self}, 
                                                               
    Path
    =ParentColumn.FieldName}"
                                  
    Value="FieldB"/>

                <Condition Binding="{Binding Converter={StaticResource compareColorConverter},
                                                               
    RelativeSource={RelativeSource self}, 
                                                                
    Path
    =ParentRow.DataContext}" 
                                   
    Value="more"/>

             </MultiDataTrigger.Conditions>

             <Setter Property="Background" Value="Red" />

          </MultiDataTrigger>

          <MultiDataTrigger>

             <MultiDataTrigger.Conditions>

                <Condition Binding="{Binding RelativeSource={RelativeSource self}, 
                                                                
    Path
    =ParentColumn.FieldName}"
                                  
    Value="FieldA"/>

                <Condition Binding="{Binding Converter={StaticResource compareColorConverter},
                                                               
    RelativeSource={RelativeSource self}, 
                                                               
    Path
    =ParentRow.DataContext}" 
                                   
    Value
    ="less"/>

              </MultiDataTrigger.Conditions>

             <Setter Property="Background" Value="Red" />

          </MultiDataTrigger>

          <MultiDataTrigger>

             <MultiDataTrigger.Conditions>

                <Condition Binding="{Binding RelativeSource={RelativeSource self}, 
                                                               
    Path
    =ParentColumn.FieldName}"
                                 
    Value="FieldB"/>

                <Condition Binding="{Binding Converter={StaticResource compareColorConverter},
                                                               
    RelativeSource={RelativeSource self}, 
                                                               
    Path
    =ParentRow.DataContext}" 
                                  
    Value
    ="less"/>

             </MultiDataTrigger.Conditions>

             <Setter Property="Background" Value="Green" />

          </MultiDataTrigger>

       </Style.Triggers>

    </Style>

    public class CompareColorConverter : IValueConverter
    {

       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
       {
          MyEntity c = value as MyEntity;

          if (c!=null)
          {
             return c.FieldA < c.FieldB ? "less" : "more";
           }

           return null;
       }

       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
       {

          string strValue = value as string;

           DateTime resultDateTime;

           if (DateTime.TryParse(strValue, out resultDateTime))
           {
              return resultDateTime;
           }

           return DependencyProperty.UnsetValue;
       }
    }

    So we have a Style that targets DataCell and uses a MultiDataTrigger with two conditions, one on the FieldName of the ParentColumn of the DataCell and the other on the DataContext of the ParentRow of the DataCell with a Converter that returns the string "less" if A is less than B and "more" if it's not. This works, the cell with the higher value between FieldA and FieldB has a red background and the other has a green background. Great! but wait... try changing the values of the cells. Notice that the colors do not update with any further changes. Why is that!? Well, it's simple, take a look at this line:

    <Condition Binding="{Binding Converter={StaticResource compareColorConverter},
                                                    RelativeSource
    ={RelativeSource self},
                                                    Path
    =ParentRow.DataContext}"
                                                    Value
    ="more"/>

    The binding in this condition is on DataContext of the ParentRow, which is the business object itself, and changing the value for its properties—FieldA or FieldB—would not cause any notifications for the DataContext and hence the compareColorConverter will not recalculate the value of the binding. So, this solution is only good if you know that the values for the fields are never going to be updated.

    For more information about how notifications work you can check Jenny's blog post here!

    So with this new information, here is my second try:

    <Style TargetType="{x:Type xcdg:DataCell}">
      <Setter Property="Background">
       <Setter.Value>
        <MultiBinding Converter="{StaticResource multiValueColorConverter}">
       <Binding RelativeSource="{RelativeSource Self}" Path="ParentColumn.FieldName"/>
         <
    Binding RelativeSource="{RelativeSource FindAncestor,

    AncestorType={x:Type xcdg:DataRow},AncestorLevel=1}" Path="DataContext.FieldA"/>
        <Binding RelativeSource="{RelativeSource FindAncestor,

    AncestorType={x:Type xcdg:DataRow},AncestorLevel=1}" Path="DataContext.FieldB"/>
        </MultiBinding>
       </Setter.Value>
       </Setter>
    </Style>

    public class MultiValueColorConverter : IMultiValueConverter
    {

        IMultiValueConverter Members

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
       
    {
          
    Brush green = Brushes.Green;
          
    Brush red = Brushes.Red;
          
    String fieldName = values[0] as string;
          
    int? fieldA = values[1] as int?;
          
    int? fieldB = values[2] as int?;

           if (fieldA != null && fieldB != null)
          
    {
             
    if (fieldName.Equals("FieldA"))
              
    {
                
    if (fieldA < fieldB)
                  
    return red;

                 return green;
             
    }
             
    else if (fieldName.Equals("FieldB"))
             
    {
                 i
    f (fieldA < fieldB)
                   
    return green;

                 return red;
             
    }
           }

           
    return null;
       }

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

    In this case, we also have a Style that targets DataCell, we use a MultiBinding to set the Value of the DataCell's Background property. The MultiBinding binds on the FieldName, the value of FieldA and the value of FieldB. The MultiValueConverter returns either a Green or a Red brush depending on the values of FieldA and FieldB and the FieldName passed to the converter. This does work even when you change the values of FieldA and FieldB at runtime, this is mainly because the binding binds directly to "FieldA" and "FieldB" properties directly and any changes would directly be notified and the converter would recalculate the binding.

    And just for the fun of it, this is a third way to do this but this time through the CellContentTemplate property of the Column.

    <xcdg:DataGridControl.Columns>
       <xcdg:Column FieldName="FieldA">
      
       <xcdg:Column.CellContentTemplate>
            
    <DataTemplate>
                
    <TextBlock Text="{Binding}">
                 
    <TextBlock.Background>
                   
    <MultiBinding Converter="{StaticResource compareColorConverter}"

    ConverterParameter='FieldA'>
                      
    <Binding Path="." />
                       
    <Binding RelativeSource="{RelativeSource FindAncestor,

    AncestorType={x:Type xcdg:Cell}}" Path="DataContext.FieldB" />
                    </MultiBinding>
               
    </TextBlock.Background>
            
    </TextBlock>
         
    </DataTemplate>
      
    </xcdg:Column.CellContentTemplate>
    </xcdg:Column>

    <xcdg:Column FieldName="FieldB">
       
    <xcdg:Column.CellContentTemplate>
          
    <DataTemplate>
             
    <TextBlock Text="{Binding}">
                
    <TextBlock.Background>
                   
    <MultiBinding Converter="{StaticResource compareColorConverter}"

    , ConverterParameter='FieldB'>
                      
    <Binding RelativeSource="{RelativeSource FindAncestor,

    AncestorType={x:Type xcdg:Cell}}", Path="DataContext.FieldA" />
                      
    <Binding Path="." />
                   
    </MultiBinding>
                
    </TextBlock.Background>
             
    </TextBlock>
         
    </DataTemplate>
      
    </xcdg:Column.CellContentTemplate>
     </xcdg:Column>
    </xcdg:DataGridControl.Columns>

    You can download the source code for the previous three samples by clicking here.

  • DataGrid Grouping Tricks

    This is my first blog at Xceed!  So I will keep it simple.... promise!
     
    This will be the first out of a series of blogs that will be posted by our support staff under the name The Tech Side. The purpose of these blogs is to demonstrate how to achieve tasks that are commonly asked by our clients.
     
    In this post from The Tech Side, I will show how to do a few tricks in Xceed DataGrid for WPF's grouping mechanism by answering the following recurring questions:

    1. How do I flatten and unflatten groups?
    2. How can I automatically hide/unhide columns when they are grouped/ungrouped?
    3. How can I prevent my end users from ungrouping a certain column while still allowing grouping and ungrouping of others?


    1- How do I flatten and unflatten groups?

    This is the simplest of all, and is often missed by many.

    You can simply set the AreGroupsFlattened property to true or false on the DataGridControl's view and, as the name implies, setting it to true would flatten the groups while setting it to false would well, unflatten them.  Here is the xaml code:

    <xcdg:DataGridControl Name="grid1"
                                             ItemsSource="{Binding Source={StaticResource techSource1}}">
       <xcdg:DataGridControl.View>
          <xcdg:TableflowView AreGroupsFlattened="True"/>
       </xcdg:DataGridControl.View>
    </xcdg:DataGridControl>

    And to toggle the AreGroupsFlattened from code behind, you can do the following:

    TableflowView view = grid1.View as TableflowView;
    view.AreGroupsFlattened = !view.AreGroupsFlattened;

    2- How can I automatically hide/unhide columns when they are grouped/ungrouped?

    For this one, you need to handle the CollectionChanged event of the GroupDescriptions property and change the corresponding column's Visible property. Here's an example:

    grid2.ItemsSourceChangeCompleted += new EventHandler(grid2_ItemsSourceChangeCompleted);

    void grid2_ItemsSourceChangeCompleted(object sender, EventArgs e)
    {
       DataGridCollectionView v ;

       v = (DataGridCollectionView)grid2.ItemsSource;
       v.GroupDescriptions.CollectionChanged += new NotifyCollectionChangedEventHandler(c_CollectionChanged);
    }

    void c_CollectionChanged ( object  sender,  NotifyCollectionChangedEventArgs e)
    {
       if (e.Action == NotifyCollectionChangedAction.Remove)
       {
          string fieldName =((DataGridGroupDescription)e.OldItems[0]). PropertyName;
          grid2.Columns[fieldName ].Visible = true;
       }
       else
       {
          string fieldName =((DataGridGroupDescription)e.NewItems[0]). PropertyName;
          grid2.Columns[fieldName ].Visible =  false;
       }
    }

    The reason why we handle ItemsSourceChangeCompleted first is because we are sure that after that event, the GroupDescriptions are initialized and that's where we handle the GroupDescriptions.CollectionChanged event.


    3- How can I prevent my end users from ungrouping a certain column while still allowing grouping and ungrouping of others?

    This one is a bit tricky, but after a few trials and errors, I was able to get it done, and in fact, the answer was pretty simple;

    This can be achieved by handling the PreviewMouseMove event on GroupByItem. In the handler, I simply check if the GroupByItem is being dragged, and if its field name corresponds to the column that I want to be un-groupable. If the 2 conditions where satisfied I simply set e.Handled = true on the event and that disables un-grouping of that column! Here's an example:

    <Style TargetType="{x:Type xcdg:GroupByItem}">
       <Style.Setters>
          <EventSetter Event="PreviewMouseMove" Handler="g_PreviewMouseMove"/>
       </Style.Setters>
    </Style>

    void g_PreviewMouseMove(object sender, MouseEventArgs e)
    {
       GroupLevelDescription gld = (GroupLevelDescription)(sender as GroupByItem).Content;

       if ((sender as GroupByItem).IsBeingDragged && gld.FieldName == "Name")
          e.Handled =  true;
    }

    You can download the source code for the previous examples here, and don't forget to leave your comments and suggestions for future blog posts in the comments section below!

    Michel

Contact | Site Map | Reviews | Legal Terms of Use | Trademarks | Privacy Statement Copyright 2011 Xceed Software Inc.