Blogs

Paul Kimmel's Blog

September 2010 - Posts

  • Pivot Grid for WinForms: Adding Custom Summary Information to a Pivot Grid

         

    This blog post is in response to a customer query on our community support site at http://community.devexpress.com/forums/p/92876/319857.aspx. The customer wrote about an all too-often scenario. The customer had data from a second table containing data that was not correlated by keys; that is, there exists no natural relationship where a join could be established and a view created. (A preferable solution in a perfect world would be to have correlated data and use views, but I have run into this scenario hundreds of times.) 

    The customer said: “I would like to append additional info at the end of each row”. The customer was kind enough to post a graphic of what he was trying to do and after a brief exchange the conversation migrated to code. As it is often easier to communicate with code then to stick to words the solution in this blog is my first attempt at getting him where he wants to be.

    A core feature of the XtraPivotGrid is that you can add all kinds of aggregate data fields at the end (or beginning) of each data section. The XtraPivotGrid supports calculating things like the Sum, Min, and Max values. These aggregate behaviors will be automatically calculated. If you want to perform custom operations then you can select the SummaryType Custom and roll your own solution. This includes adding fields that contain unrelated or only quasi-related data. This is a slightly one-off solution—displaying uncorrelated data—which is why we need a little bit of code to get the job done. Figure 1 shows the end result. Then, we’ll walk through the steps to prepare the solution, followed by the code.

    image
    Figure 1: Note that the end of the group of data the summary data is followed by the extra, unrelated data.

    Using data that is technically unrelated may not make technical sense but that doesn’t mean it won’t make sense to users or that customers won’t want such a solution.

    In Figure 1 the Products table data is displayed with the Category ID and Product Name in the row area and the Unit Price in the data area. The total Unit Price is calculated by category and product. (Probably not a practical application—sum of unit prices—but it shows how summary information is automatically available in the XtraPivotGrid.) The three additional summary values could automatically be things like averages, minimum or maximum unit prices, or perhaps the number of items per category. For our scenario however we are adding some arbitrary data representing data that is not technically related but makes sense to a user. Here are the steps for adding additional custom totals followed by one solution for putting data in them.

    Adding Custom Totals to a Pivot Grid

    The following steps demonstrate how to add custom totals to an XtraPivotGrid and write code that puts programmer-defined values in the custom totals fields.

    1. Create a WinForms project
    2. Add an PivotGridControl and dock it to the Form
    3. Use the PivotGridControl’s smart tags menu to configure the Northwind Products table to the pivot grid; this will add a northwindDataSet, productsBindingSource, and productsTableAdapter to the components tray in Visual Studio
    4. Right-click on the pivot grid and select Run Designer
    5. Close the designer
    6. Click on the Properties Window and select the fieldCategory1 control
    7. Click the Custom Totals property to open the designer-see Figure 2
    8. In the PivotGridCustomTotal Collection Editor click Add three times to add three custom totals
    9. For each of the three custom totals change the Tag properties to 1, 2, 3, respectively and the SummaryType to Custom
    10. Click OK to close the Collection Editor
    11. Back in the Properties window for the fieldCategoryID1 control—a PivotGridField object—change the TotalsVisibility to CustomTotals
    12. Click on the PivotGridControl
    13. Switch the Properties window view to the Events tab
    14. Double click to implement a CustomCellValue event and a CustomDrawFieldValue event methods
    15. Use CustomDrawFieldValue to change the custom summary captions, and use CustomCellValue to add the custom cell values for each custom summary field

    image
    Figure 2: Add three custom totals for the CategoryID field to the PivotGridCustomTotal Collection Editor .

    The CustomCellValue event is used to do whatever it takes to correlate data where a natural correlation does not exist. In this example I contrived some arbitrary data and used the CategoryID’s to index the list of other data. Listing 1 contains the complete solution.

    Listing 1: The custom events modify the custom total fields to contain data that is not ‘exactly’ aggregate values, but logically related to the records in some way—determined by a user scenario.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using DevExpress.XtraPivotGrid;
    using DevExpress.Data.PivotGrid;
    using System.Reflection;
    
    namespace S319857
    {
      public partial class Form1 : Form
      {
        private List<Data> datum;
        public Form1()
        {
          InitializeComponent();
    
          datum = new List<Data>{
            new Data{ID=1, Cash="100", EftPos="Dummy", HealthFund="H1"},
            new Data{ID=2, Cash="200", EftPos="Dummy", HealthFund="H2"},
            new Data{ID=3, Cash="300", EftPos="Dummy", HealthFund="H3"},
            new Data{ID=4, Cash="400", EftPos="Dummy", HealthFund="H4"},
            new Data{ID=5, Cash="500", EftPos="Dummy", HealthFund="H5"},
            new Data{ID=6, Cash="600", EftPos="Dummy", HealthFund="H6"},
            new Data{ID=7, Cash="700", EftPos="Dummy", HealthFund="H7"},
            new Data{ID=8, Cash="800", EftPos="Dummy", HealthFund="H8"}};
        }
    
        private void Form1_Load(object sender, EventArgs e)
        {
          // TODO: This line of code loads data into the 
          // 'northwindDataSet.Products' table. You can move, or remove it, as needed.
          this.productsTableAdapter.Fill(this.northwindDataSet.Products);
    
        }
    
        private void pivotGridControl1_CustomCellValue(object sender, 
          PivotCellValueEventArgs e)
        {
          int ID = Convert.ToInt32(e.GetFieldValue(fieldCategoryID1));
    
          Data obj = datum.Find(d => d.ID == ID);
    
          if ((e.SummaryType == PivotSummaryType.Custom) 
            && (e.DataField == fieldUnitPrice1))
          {
            if (Convert.ToInt32(e.RowCustomTotal.Tag) == 1)
              e.Value = obj.Cash;
            else if (Convert.ToInt32(e.RowCustomTotal.Tag) == 2)
              e.Value = obj.EftPos;
            else if (Convert.ToInt32(e.RowCustomTotal.Tag) == 3)
              e.Value = obj.HealthFund;
          }
        }
    
        private void pivotGridControl1_CustomDrawFieldValue(object sender, 
          PivotCustomDrawFieldValueEventArgs e)
        {
          if (e.Info.Caption.Contains("Custom"))
          {
            if (Convert.ToInt32(e.CustomTotal.Tag) == 1)
              e.Info.Caption = "CASH";
            else if (Convert.ToInt32(e.CustomTotal.Tag) == 2)
              e.Info.Caption = "EFTPOS";
            else if (Convert.ToInt32(e.CustomTotal.Tag) == 3)
              e.Info.Caption = "Health Fund";      
          }
        }
      }
    
    
      public class Data
      {
        private int iD;
        public int ID
        {
          get { return iD; }
          set { iD = value; }
        }
    
        private string cash;
        public string Cash
        {
          get { return cash; }
          set { cash = value; }
        }
    
        private string eftPost;
        public string EftPos
        {
          get { return eftPost; }
          set { eftPost = value; }
        }
    
        private string healthFund;
        public string HealthFund
        {
          get { return healthFund; }
          set { healthFund = value; }
        }
      }
    
    }

    The CustomCellValue event uses the event argument’s GetFieldValue and the control who’s value you want to get as a parameter to get the current field value which is the CatorgoryID in this instance. datum.Find uses a Lambda expression to find the right Data object—representing arbitrary data. Finally, the if..conditional uses the predetermined Tag values to figure out which total’s fields get which data. CustomDrawField uses the Tags again to modify the custom total’s fields captions.

    If you have naturally correlated data—disparate tables with primary and foreign keys—then you can use views and joins to populate the PivotGridControl. If you have a customers that have legacy data or just want to show something else then you can exploit the custom totals feature and events to get that data in the PivotGridControl.

    Thanks for choosing Developer Express.

  • ADO.NET Entity Framework 4 and Pivot Grid Control – Create a Model Defined Function and Bind Results to Pivot Grid

         

    As a technophile who also happens to write I know how hard it can be when the documentation for a new technology is squiffy. Squiffy being you can’t figure it out from existing documentation and blogs because too many details are left out. This what happened when I started looking at EF4 and Model Defined Functions. A model “declared” function is one that you import from your storage provider, like a SQL Server stored procedure. A model defined function is one that is completely defined in your model. EF4 supports model defined functions but the documentation leaves out too many details that explain exactly how you get this to work. In this blog I will walk you through creating a model defined function in EF4 and then binding the results to our XtraPivotGrid.

    An overview of the process requires some data you want to show in a Pivot Grid, an idea for tweaking the result set, and Entity Framework 4 model, and a custom query defined in the model only. The two basic steps are to add an XtraPivotGrid control to a WinForms project and add an ADO.NET Entity Data Model. (This example is based on Visual Studio 2010 and DevExpress controls—I used the a beta version of DevExpress controls for 10.2.) There are a lot of blogs and articles on EF Models but here is the nuts and bolts. (If you know how to do this step skip the next section.)

    Adding an Entity Framework Model to Your WinForms Application

    An Entity Framework Data Model is created from an applet in the Add New Items dialog. In its simplest form an EF model is simply some code generated classes that create entities based on some storage item like a database table. The basic idea is that once you have the data model interaction with the data store is easier. (We all refer to this as a data access layer in a general sense.)

    To create the entity data model Follow these steps to create:

    1. Create a WinForms project. (This will work in other kinds of projects too.)
    2. Click Project|Add New Item
    3. Select the ADO.NET Entity Data Model from the Add New Item dialog and name it NorthwindModel
    4. Click Add
    5. In the Entity Data Model Wizard (that starts up) select Generate from database and click Next
    6. Choose an existing Northwind connection or click New Connection to add a connection
    7. Use the defaults for everything else in this step and click Next
    8. In the Choose Your Data Objects step expand Tables and select the Products table
    9. Click Finish

    That’s all there is to it. After you click Finish the .edmx file and designer will be open. An entity model is the important element you need.

    Defining a Model Defined Function

    A model defined function is one that exists only in your model. A model declared function is one you import from your server. We will be creating a model defined function. For our scenario we will add a model defined function that filters on ReorderLevel. It is worth stating that you could add such a trivial filter right in the LINQ query, but we will create the model defined function and use such a simple example to focus on the mechanics of model functions. As a general rule I would only add a model defined function if it gave my solution some extra leverage that might be hard for my developers to do themselves with LINQ like joins.

    The key to adding a model defined function is to modify the model XML, but a lot of the blogs don’t tell you which XML to modify and leave out a few steps. You should be able to reproduce this process because I am not going to leave any steps out. To create the model defined function you need to modify the SSDL in the EDMX file and then import the function and call it with your LINQ query, binding the results to the PivotGrid. Here they are:

    1. Right click on the .EDMX file in the solution explorer and select Open With
    2. Choose the XML (Text) Editor—yep, we gotta modify the EDMX file directly
    3. The EDMX file will look like Listing 1. Find the SSDL section
    4. At the end of the SSDL section just before the closing </Schema> tag add a <Function> tag
    5. Add a Name attribute Name=”GetProductsByReorderLevel”
    6. Add an IsComposable=”false” attribute
    7. In the <Function> tag add a <CommandText> tag and write your vanilla SQL inside of the <CommandText></CommandText> tag; I used select * from Products where ReorderLevel > @p1
    8. Make sure you use an @ in the query itself or the framework will think the right hand side operand is a column
    9. After the <CommandText>—parameters are expected to follow—add a <Parameter> tag with a Name=”p1”, Mode=”In”, and Type=”int” attributes. (Make sure you don’t use @ in the parameter Name attribute; doing so will kill the code generation.)

    Here is the added XML block and Listing 1 shows the complete EDMX listing.

    <Function Name="GetProductsByReorderLevel" IsComposable="false">
      <CommandText>
       select * FROM Products
       where ReorderLevel > @p1;
      </CommandText>
      <Parameter Name="p1" Mode="In" Type="int" MaxLength="75" />
    </Function>
    

    Listing 1: The complete .EDMX listing with the SSDL model defined function.

    <?xml version="1.0" encoding="utf-8"?>
    <edmx:Edmx Version="2.0" xmlns:edmx="http://schemas.microsoft.com/ado/2008/10/edmx">
      <!-- EF Runtime content -->
      <edmx:Runtime>
        <!-- SSDL content -->
        <edmx:StorageModels>
          <Schema Namespace="NorthwindModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2008" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">
            <EntityContainer Name="NorthwindModelStoreContainer">
              <EntitySet Name="Products" EntityType="NorthwindModel.Store.Products" store:Type="Tables" Schema="dbo" />
            </EntityContainer>
            <EntityType Name="Products">
              <Key>
                <PropertyRef Name="ProductID" />
              </Key>
              <Property Name="ProductID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
              <Property Name="ProductName" Type="nvarchar" Nullable="false" MaxLength="40" />
              <Property Name="SupplierID" Type="int" />
              <Property Name="CategoryID" Type="int" />
              <Property Name="QuantityPerUnit" Type="nvarchar" MaxLength="20" />
              <Property Name="UnitPrice" Type="money" />
              <Property Name="UnitsInStock" Type="smallint" />
              <Property Name="UnitsOnOrder" Type="smallint" />
              <Property Name="ReorderLevel" Type="smallint" />
              <Property Name="Discontinued" Type="bit" Nullable="false" />
            </EntityType>
            <Function Name="GetProductsByReorderLevel" IsComposable="false">
              <CommandText>
                select * FROM Products
                where ReorderLevel > @p1;
              </CommandText>
              <Parameter Name="p1" Mode="In" Type="int" MaxLength="75" />
            </Function>
          </Schema>
        </edmx:StorageModels>
        <!-- CSDL content -->
        <edmx:ConceptualModels>
          <Schema Namespace="NorthwindModel" Alias="Self"
          xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
          xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
            <EntityContainer Name="NorthwindEntities" annotation:LazyLoadingEnabled="true">
              <EntitySet Name="Products" EntityType="NorthwindModel.Product" />
            </EntityContainer>
            <EntityType Name="Product">
              <Key>
                <PropertyRef Name="ProductID" />
              </Key>
              <Property Name="ProductID" Type="Int32" Nullable="false"
              annotation:StoreGeneratedPattern="Identity" />
              <Property Name="ProductName" Type="String" Nullable="false"
              MaxLength="40" Unicode="true" FixedLength="false" />
              <Property Name="SupplierID" Type="Int32" />
              <Property Name="CategoryID" Type="Int32" />
              <Property Name="QuantityPerUnit" Type="String" MaxLength="20"
              Unicode="true" FixedLength="false" />
              <Property Name="UnitPrice" Type="Decimal" Precision="19" Scale="4" />
              <Property Name="UnitsInStock" Type="Int16" />
              <Property Name="UnitsOnOrder" Type="Int16" />
              <Property Name="ReorderLevel" Type="Int16" />
              <Property Name="Discontinued" Type="Boolean" Nullable="false" />
            </EntityType>
          </Schema>
        </edmx:ConceptualModels>
        <!-- C-S mapping content -->
        <edmx:Mappings>
          <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
            <EntityContainerMapping StorageEntityContainer="NorthwindModelStoreContainer"
            CdmEntityContainer="NorthwindEntities">
              <EntitySetMapping Name="Products"><EntityTypeMapping
              TypeName="NorthwindModel.Product"><MappingFragment StoreEntitySet="Products">
                <ScalarProperty Name="ProductID" ColumnName="ProductID" />
                <ScalarProperty Name="ProductName" ColumnName="ProductName" />
                <ScalarProperty Name="SupplierID" ColumnName="SupplierID" />
                <ScalarProperty Name="CategoryID" ColumnName="CategoryID" />
                <ScalarProperty Name="QuantityPerUnit" ColumnName="QuantityPerUnit" />
                <ScalarProperty Name="UnitPrice" ColumnName="UnitPrice" />
                <ScalarProperty Name="UnitsInStock" ColumnName="UnitsInStock" />
                <ScalarProperty Name="UnitsOnOrder" ColumnName="UnitsOnOrder" />
                <ScalarProperty Name="ReorderLevel" ColumnName="ReorderLevel" />
                <ScalarProperty Name="Discontinued" ColumnName="Discontinued" />
              </MappingFragment></EntityTypeMapping></EntitySetMapping>
            </EntityContainerMapping>
          </Mapping>
        </edmx:Mappings>
      </edmx:Runtime>
      <!-- EF Designer content (DO NOT EDIT MANUALLY BELOW HERE) -->
      <Designer xmlns="http://schemas.microsoft.com/ado/2008/10/edmx">
        <Connection>
          <DesignerInfoPropertySet>
            <DesignerProperty Name="MetadataArtifactProcessing" Value="EmbedInOutputAssembly" />
          </DesignerInfoPropertySet>
        </Connection>
        <Options>
          <DesignerInfoPropertySet>
            <DesignerProperty Name="ValidateOnBuild" Value="true" />
            <DesignerProperty Name="EnablePluralization" Value="True" />
            <DesignerProperty Name="IncludeForeignKeysInModel" Value="True" />
          </DesignerInfoPropertySet>
        </Options>
        <!-- Diagram content (shape and connector positions) -->
        <Diagrams>
          <Diagram Name="NorthwindModel">
            <EntityTypeShape EntityType="NorthwindModel.Product" Width="1.5" PointX="0.75"
            PointY="0.75" Height="2.9417936197916665" IsExpanded="true" />
          </Diagram>
        </Diagrams>
      </Designer>
    </edmx:Edmx>

    I intentionally did not use code formatting and re-paginated the XML to fit the page, but don’t do that. Just add the <Function> block in bold in Listing 1.

    One might think that is all there is too it, but the Entity Framework maps at the the conceptual storage level, CSDL, and we added the model function at the storage level. You need to map the function at the conceptual level next.

    Before you continue close the .edmx file. Make sure you save it. Then, double-click the .edmx file to open the designer, right click, and click Validate to make sure your XML is good.

    Mapping the Model Defined Function at the Conceptual Level

    Your model-defined function at the SSDL should now be available. To view the model select View|Other Windows|Entity Data Model Browser. If everything is going well so far your function should be available in the Model Browser under Northwind.Store|Stored Procedures—see Figure 1.

    ADO.NET Model Browser

    Figure 1: The model-defined function at the storage level.

    The next step is to import the function, which in essence adds an entry to the CSDL level and let’s you call it. To add the function at the conceptual level right click on the NorthwindModel.edmx file and follow these steps:

    1. Click Add|Function Imports
    2. Using Figure 2 as a visual guide, enter a function name. I used GetProductsByReorderLevel, but if you have funky Sproc names then you can make them more code friendly here.
    3. Select the Stored Procedure Name
    4. Change the Returns a Collection of to Entities and select the Product entity type
    5. Click OK

    Now if you check the Model Browser you will see that the Function Imports section contains a usable CSDL function—see Figure 3.

    Add Function Import dialog
    Figure 2: Add Function Import to add the function to the CSDL-conceptual level—so you can call it with LINQ to Entities.

    Model Browser with CSDL Level Model Defined Function
    Figure 3: The function GetProductsByReorderLevel can now be called from LINQ to Entities.

    Binding the PivotGrid to the Model Defined Function

    Unfortunately the data source wizard in Visual Studio 2010 doesn’t know about model defined functions so we can’t use the wizard to get the data from the model defined function. However, since the model-defined function is mapped to the Product entity you can use the Data Source Configuration Wizard to bind to an Object. Follow these steps to configure the PivotGrid with Product entity data.

    1. Click the PivotGridControl’s smart tags menu, click Choose Data Source|Add Project Data Source
    2. In the Data Source Configuration Wizard choose Object
    3. Click Next
    4. Expand the project’s node
    5. Expand the first child node and check Product
    6. Click Finish

    Use the Data Source Configuration Wizard to set the XtraPivotGrid's Fields
    Figure 4: Let the Data Source Configuration Wizard configure the XtraPivotGrid’s fields for you.

    In the code behind you need just a couple of lines in the code behind and you are off to the races.

    Listing 2 initializes the NorthwindEntities object, calls the model-defined function, and updates the productBindingSource.DataSource to point to the new data. It takes some work to get here but most of the effort was completed by wizards with just a tiny bit of XML editing and a very little bit of code. Since code is generally the most time consuming thing and the most expensive this is actually a beautiful thing.

    Listing 2: Once you get your entities right it takes just a little bit of code to use them.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    namespace ModelDefinedFunctionAndPivotGrid
    {
      public partial class Form1 : Form
      {
        NorthwindEntities entities = new NorthwindEntities();
        public Form1()
        {
          InitializeComponent();
    
          var results = from product in entities.GetProductsByReorderLevel(10)
                        select product;
          productBindingSource.DataSource  = results;
        }
      }
    }

    I’d love to see an SSDL designer for model defined functions. I’d also like to eliminate the leap from SSDL to CSDL. It doesn’t make a lot of sense that one would add a function they didn’t want to use from LINQ, but EF4 is a big improvement over earlier versions. Figure 5 shows the running application after I have moved some fields around at runtime.

    How Sweet It Is

    Figure 4: The XtraPivotGrid with data from a model-defined function and a single LINQ query.

  • Windows Forms Pivot Grid Control – Using the CustomFilterPopupItems Event to Manage Filter Items in 10.2

         

    I manage a TODO list with Microsoft Outlook. If it weren’t for Outlook and Calendar items synched on my iPhone I would forgot a lot of detailed tasks. Right now there are so many new features for 10.2 that I want to cover that my task list has gotten pretty long and is growing and projecting out for several weeks to come. It is long enough right now that I wonder if I am going to get some sleep this weekend or get to that 30 foot trunk of a tree that the wind just knocked over into my yard. The last week or so sleep has been at the bottom of the TODO list—although I did wheeze through 30 minutes of basketball at the gym last night.

    The feature on the menu today is a single feature, the CustomFilterPopupItems event for the XtraPivotGrid. The XtraPivotGrid is a composable, searchable, sortable, filterable grid that supports data mining type tasks for your end users. The CustomFilterPopupItems event is new in 10.2 and is fired when the user displays the filter list for a field. Figure 1 shows the demo application with the filter list visible. A user brings up the filter list by clicking on the little filter button associated with a given field.

    Pivot Grid Control with Beverages Category Checked
    Figure 1: Pivot Grid Control with the filter items visible for the Category Name field. 

    This demo handles the CustomFilterPopupItems event for the XtraPivotGrid control. Normally when you display the filter and deselect items those elements are removed from the result set—the data area in the grid and the row area. For example, if you expand the Category Name filter and uncheck Show All and check Beverages then the default behavior is that the products that are beverages are the only products shown .

    By default the non-visible data items are available in the filter still. This sample demonstrates how to hide the checked filter items that aren’t visible in the grid. For example, in our scenario by checking the Beverages Category only beverages are visible in the grid, and with the sample code for CustomFilterPopupItems in Listing 1 the invisible items are also removed from the filter list. For instance, if you drop the Category Name filter in our scenario then the event code removes items that for all intents and purposes won’t be displayed anyway.

    Figure 2 shows the Beverages scenario without the event code, and Figure 3 shows the scenario with the event code.

    Without the CustomFilterPopupItems Event All Products are Listed in the Filter  
    Figure 2: Notice that even though Beverages is the only Category checked all products are visible in the Product Name filter.

    With the CustomFilterPopupItems Event Only Beverages are Shon in the Product Filter
    Figure 3: With the CustomFilterPopupItems event (as written in Listing 1) invisible items are removed from the popup filter list.

    Listing 1: Using the CustomFilterPopupItems event to hide filter items that aren’t being displayed in the data grid based on other filter selections.

    using System;
    
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    using DevExpress.XtraPivotGrid.Data;
    
    namespace EmptyWinApp {
        public partial class Form1 : Form {
            public Form1() {
                InitializeComponent();
            }
    
            private void Form1_Load(object sender, EventArgs e) {
                // TODO: This line of code loads data into the 'productReports._ProductReports' 
                // table. You can move, or remove it, as needed.
                this.productReportsTableAdapter.Fill(this.productReports._ProductReports);
            }
    
            private void pivotGridControl1_CustomFilterPopupItems(object sender, 
            DevExpress.XtraPivotGrid.PivotCustomFilterPopupItemsEventArgs e) 
            {
                List<object> values = e.Field.GetVisibleValues();
                values.Sort();
                for(int i = e.Items.Count - 1; i >= 0; i--) {
                    if(e.Items[i].IsChecked == true && values.BinarySearch(e.Items[i].Value) < 0)
                        e.Items.RemoveAt(i);
                }
            }
        }
    }

  • Windows Forms Grid Control – Querying and Changing Grid Control Data With LINQ

         

    We love customers at DevExpress, so when one of you guys writes me from the trenches I drop what I am doing and try to get a response out as soon as possible.

    Today, while working on a blog post for the ASP.NET Pivot Grid Control’s (XtraPivotGrid) CustomFilterPopupItems event for 10.2 a customer sent me a message through the newsgroups. He wanted to know if it was possible to use LINQ not as a datasource for an XtraGrid but to manipulate data in an XtraGrid. As is customary I said if you send me a trimmed down demo then I will tweak it for you. This approach is mostly to ensure that I understand what the customer is trying to do. In this case I had a pretty good idea of what he was trying to accomplish so I whipped up a solution and some explanatory text that gives one an idea of how to approach a solution using LINQ.

    LINQ uses deferred execution. This means LINQ won’t actually touch the data in the query until you start accessing it. A LINQ query itself runs when the data is accessed not at the time (or line) where the query exists unless you touch it in the query, like calling an extension method like First, or Take on the tail end of the query. It is also worth noting that LINQ is for querying not updating data necessarily, so updating something else in a LINQ query requires using side effects.

    For the solution I demonstrated the side effects approach in a compiler directive #if and a preferable, more reader friendly solution in the #else condition. Both approaches are explained along with some sidebar discussions about the code in general—see Listing 1.

    Listing 1: Touching data in an XtraGrid with a LINQ query.
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    
    namespace QueryXtraGridWithLINQ
    {
      public partial class Form1 : Form
      {
        public Form1()
        {
          InitializeComponent();
        }
    
        private void Form1_Load(object sender, EventArgs e)
        {
          // TODO: This line of code loads data into the 'northwindDataSet.Orders' table. 
          // You can move, or remove it, as needed.
          this.ordersTableAdapter.Fill(this.northwindDataSet.Orders);
    
        }
    
        private void gridControl1_Click(object sender, EventArgs e)
        {
    #if   TOGGLE
          // do something with LINQ on side effects but not a good idea
          Func<DataRow, DateTime> SideEffects = row => 
          {
            DateTime newDateTime = Convert.ToDateTime(row["OrderDate"]).AddYears(1);
            object[] data = row.ItemArray;
            data[3] = newDateTime;
            northwindDataSet.Orders.LoadDataRow(data, false);
            return newDateTime;
          };
          
          // you could try this but because of deferred execution side effects 
          // won't actually happen until you touch the data
          var orderDates = from DataRow row in northwindDataSet.Orders.Rows
                           select new {OrderID=row["OrderID"], OrderDate=SideEffects(row)};
    
          // this is mumbo jumbo - esoterrorism
          orderDates.ToArray();
          int rowsUpdated = ordersTableAdapter.Update(northwindDataSet.Orders);
          gridControl1.Refresh();
          MessageBox.Show(string.Format("rows {0} updated", rowsUpdated));
    #else
          
          // a better solution
          var orderDates = from DataRow row in northwindDataSet.Orders.Rows
                           select new {OrderID=row["OrderID"], 
                           OrderDate=Convert.ToDateTime(row["OrderDate"]).AddYears(1), 
                           RowData=row};
    
          foreach(var r in orderDates)
          {
            object[] data = r.RowData.ItemArray;
            // get desired data element to change - this is the least reliable way
            data[3] = r.OrderDate;
            northwindDataSet.Orders.LoadDataRow(data, false);
          }
    
          Validate();
          ordersBindingSource.EndEdit();
          int rowsUpdated = ordersTableAdapter.Update(northwindDataSet.Orders);
          gridControl1.Refresh();
          MessageBox.Show(string.Format("rows {0} updated", rowsUpdated));
    #endif
        }
      }
    }

    In the #if TOGGLE part of the code a generic delegate (Func) and a Lambda Expressions are used to update an arbitrary column’s value, in this instance OrderDate. The DataRow comes in, the new data is set, and data is updated with LoadDataRow. The Lambda Expression returns the new data and time value, so it can be assigned to the anonymous type’s projection value OrderDate in the select clause of the LINQ query. Using data[3] is risky so in general a better approach to identifying the correct column and table should be used. But this approach has bigger problems.

    Func SideEffects has side effects, that is, it changes data. Doing unexpected things is generally considered a no no. Worse still the LINQ query (because LINQ uses deferred execution) won’t even run unless we touch it. In this case touching it is accomplished by calling orderDates.ToArray. ToArray actually causes the query to execute. This approach while functional is esoteric to say the least. So, it is possible to use LINQ directly to change data, but I wouldn’t use this approach.

    The #else block contains a better approach.

    In the #else block LINQ is used to get the data to be changed, but a for each is still used to actually effect the change. This is a better solution because we need to touch the data to execute the query anyway, and in the LINQ query itself updates were not intended. of course, as is shown in the projection—select new—you can update the results in the new, projected data.

    Next, the foreach statement iterates over all of the data and updates the OrderDate value. Storing the DataRow in the projection and using LoadDataRow is a nice, convenient way to update the desired rows. If you wanted to update just some rows then use a where clause in the LINQ query to just get the data you wanted. TableAdapter.Update and Refresh will ensure that the database and the GridControl has the new values.

    Using the smart tags menu for the ordersBindingSource an Update method was added to the XSD for the solution. Refer to the code attachment for the complete solution.

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.