Blogs

Paul Kimmel's Blog

June 2010 - Posts

  • Printing Data from an XtraPivotGrid

         

    I like the XtraPivotGrid. The XtraPivotGrid lets users create crosstab views of data that permit alternative ways to look at the same data. More than that I like the XtraPivotGrid for its end user composability. It is one of those controls that naturally lend itself to end user customization post delivery. By dragging and dropping data elements to row, column, data or filter areas a user can completely change the layout of the data and look at the data from any perspective that makes sense to the user. For the developer the XtraPivotGrid means that the amount of time spent at design time can be reduced; bind the XtraPivotGrid to a data source (or OLAP) source and let the user tinker with the appearance.

    If you combine the end user composability factor with built in printing capabilities a user can take a code-light XtraPivotGrid form and turn it into an ad hoc reporting engine. All of this capability is built into the XtraPivotGrid. As a developer you just need to obtain your copy of DXperience (or the XtraPivotGrid) an you get these features and your customers can have access to the power and flexibility without necessitating writing a ton of code on your part. the following steps walk you through creating a WinForms application with an XtraPivotGrid and turn it into an ad hoc reporting tool with a minimum of effort.

    To create the demo (shown in Figure 1) follow these steps:

    1. Create a WinForms application in Visual Studio 2010

    2. Drag a DevExpress RibbonControl onto the form. (This will be used to initiate printing)

    3. Use the RibbonControl designer to add a BarButtonItem to a RibbonPageGroup (see Figure 2)

    4. Switch to the BarButtonItem’s event’s view and implement the ItemClick event. (See Listing 1 for the code for the ItemClick event)

    5. Drag a PivotGridControl to the form and change the Dock style to Fill.

    6. From the PivotGridControl’s smart tags menu select Choose Data Source, and click Add Project Data Source

    7. Configure the Northwind database’s Products table

    8. Add the code to the ItemClick event as show in Listing 1

    9. Run the demo

    Figure 1 shows the runtime view and Figure 3 shows the design time view. Notice that in this demo we didn’t even set the columns to any of teh areas in the PivotGridControl. We will let the end user decide where to display the data elements at runtime.

    A default view of the XtraPivotGrid
    figure 1: The runtime view prior to setting the data elements to the desired areas of the XtraPivotGrid.

    DevExpress RibbonControl Designer
    Figure 2: Add a BarButtonItem to the RibbonControl and implement the ItemClick event for the print action.

    A RibbonControl and PivotGridControl
    Figure 3: The design time view of the demo.

    Listing 1: the code-behind for the ItemClick event.

    Public Class Form1
    
      Private Sub Form1_Load(ByVal sender As System.Object,
        ByVal e As System.EventArgs) Handles MyBase.Load
        'TODO: This line of code loads data into the 'NorthwindDataSet.Products' table. 
    'You can move, or remove it, as needed.
    Me.ProductsTableAdapter.Fill(Me.NorthwindDataSet.Products) End Sub Private Sub BarButtonItem1_ItemClick(ByVal sender As System.Object, ByVal e As DevExpress.XtraBars.ItemClickEventArgs) Handles BarButtonItem1.ItemClick PivotGridControl1.OptionsPrint.VerticalContentSplitting = DevExpress.XtraPrinting.VerticalContentSplitting.Smart PivotGridControl1.OptionsPrint.PageSettings.Landscape = True PivotGridControl1.Print() End Sub End Class

     

    Clearly the user will want to move data elements around and configure a layout that tells them a user story about the data. Follow these steps to configure one view of the data.

    1. In this order drag the CategoryID to the area that reads Drop Row Fields Here

    2. Drag the SupplierID next to the CategoryID

    3. Drag the ProductName next to the SupplierID

    4. Drag and drop the Units In Stock, Unit Price and Units On Order fields to the area that reads Drop Data Items

    5. Collapse a few of the categories so you can see the totals and grand totals row—see Figure 4

    6. Click the print button

    Figure 5 shows the first page of the printed report. For an ad hoc report this conveys the data desired in the layout desired and it looks pretty good for a no-code solution. If you want more control over the “report” then consider switching to XtraReports. However, a lot of time users need ad hoc reports as often as they need pre-determined reports, and the built in printing capabilities make ad hoc reporting a reality with little or no effort from you.

    The runtime view with fields moved around by the user
    Figure 4: The view after following steps 1 through 6 above.

    The printed ad hoc report
    Figure 5: An ad hoc report fully created by the end user.

    If you haven’t used the XtraPivotGrid before there are a few features I’d like to point out before we wrap up. Click any of the field buttons and a filter tag menu pops up. This feature let’s the user pick the data results for that field, including all or individual fields. Click field buttons in the row area to change the sort order of the that row. Right-click the a field button to display a context menu. the context menu let’s the user Reload data, hide, change the order, show the field list (to retrieve hidden field’s), and show a PivotGrid Prefilter dialog that enables the end user to build custom data filters visually (see Figure 6).

    Built in PivotGrid Prefilter supports end user "where" clauses
    Figure 6: Use the PivotGrid Prefilter at runtime to define custom data filters. (In the example  only categories greater than 5 are shown).

    After you apply a filter the filter is shown at the bottom of the XtraPivotGrid and it can be disabled and re-enabled or deleted by the end user (see Figure 7).

    End user definable filters are built into the XtraPivotgrid
    Figure 7: Enable, disable, or delete prefilters defined by the end user here.

  • Covariance and Contravariance in .NET 4.0

         

    There are a ton of blog posts on covariance and contravariance. These subjects are based on math principles of the same name and applied in an analogous way to .NET 4.0 programming. The rub is that a lot of the blog posts and the help files seem to do a good job of explaining assignment compatibility more than covariance and contravariance. the most common definition goes something like this: suppose you have a parent class Person and child classes Employee and Customer that inherit from Person. A Person could be a Customer too, so it is a wider type relative to Employee and Customer and Employee and Customer are narrower types. It makes sense that you can declare a Person and assign it an instance of a Customer or Employee. This underpins how polymorphism is supported but describes assignment compatibility.

    A second part of the definition is that covariance preserves assignment compatibility and contravariance is the opposite of regular child to parent assignment. This is a little better but not entirely or tangibly concrete. To make these subjects a little more concrete and hopefully less abstruse I will use an exemplar for both topics, illustrating what one might intuitively like to do but couldn’t prior to .NET 4.0. Using the class diagram in Figure 1 and generic delegates and Lambda Expressions (for simplicity) let’s examine the before and after support for variance.

     

    A simple class diagram showing inheritance relationships
    Figure 1: A simple class diagram showing inheritance relationships.

    In pre .NET 4.0 you could define a delegate with a generic parameter, specify the parameter type. Suppose that parameter type was a child type—Customer in our example. It seems intuitive that the same delegate could be assigned to a second delegate that accepted a parent type. Unfortunately this didn’t work—see Listing 1.

    Listing 1: Pre .NET 4.0 this seemingly intuitive assignment didn’t work.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace VarianceDemo
    {
      class Program
      {
    
        delegate T MyFunc<T>();
    
        static void Main(string[] args)
        {
          // attempt at covariance
          MyFunc<Customer> getCustomer = ()=>new Customer();
          MyFunc<Person> getPerson = getCustomer;
    
          Console.ReadLine();
    
        }
      }
    
      class Person{}
      class Customer : Person{}
      class Employee: Person{}
      class Manager: Employee{}
    }

    It really seems like a function that returns a a more derived type—Customer—should be compatible with a function that uses a less derived type, a parent class. As is this code doesn’t work and didn’t work prior to .NET 4.0. However, if you change the generic parameter T to an out parameter in .NET 4.0 then assignment compatibility via covariance is supported. Listing 2 contains the very simply revision.

    Listing 2: Change the generic parameter to an out parameter to support covariance—narrower type to wider type assignment.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace VarianceDemo
    {
      class Program
      {
    
        delegate T MyFunc<out T>(); // use out modifier
    
        static void Main(string[] args)
        {
          // covariance
          MyFunc<Customer> getCustomer = ()=>new Customer();
          MyFunc<Person> getPerson = getCustomer;
    
          Console.ReadLine();
    
        }
      }
    
      class Person{}
      class Customer : Person{}
      class Employee: Person{}
      class Manager: Employee{}
    
    }

    The only change to the code is the use of the out modifier in the delegate definition. (The use of the Lambda expression is for convenience only.) Now getPerson will return a Customer which is after all a kind of Person.

    Contravariance supports the opposite direction of assignment. For instance, you might like to define a delegate that performs an action on a Person and assign that to a delegate that performs that action on a Customer. Intuit9ively, you might write the code in Listing 3.

    Listing 3: An attempt at contravariance—assigning a wider type to a narrower type.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace VarianceDemo
    {
      class Program
      {
    
        delegate void MyAction<T>(T t);
    
        static void Main(string[] args)
        {
    
          // attempt at contravariance
          MyAction<Person> printPerson = (person)=>Console.WriteLine(person);
          MyAction<Customer> printCustomer = printPerson;
    
    
          Console.ReadLine();
    
        }
      }
    
      class Person{}
      class Customer : Person{}
      class Employee: Person{}
      class Manager: Employee{}
    }

    This seems intuitive too. Why not? A Customer is a kind of Person and it is likely that you might want to perform the same kinds of actions on Customers as you would Persons. Prior to .NET 4.0 this didn’t work, but if you change the generic parameter to an in parameter then assignment compatibility in the form of contravariance is supported—see Listing 3.

    Listing 3: With the in modifier on delegate parameter assignment from a wider type to a narrower type works too.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace VarianceDemo
    {
      class Program
      {
    
        delegate void MyAction<in T>(T t);
    
        static void Main(string[] args)
        {
          // contravariance
          MyAction<Person> printPerson = (person)=>Console.WriteLine(person);
          MyAction<Customer> printCustomer = printPerson;
    
          Console.ReadLine();
        }
      }
    
      class Person{}
      class Customer : Person{}
      class Employee: Person{}
      class Manager: Employee{}
    
    }

    In the revision printCustomer can piggyback on the printPerson behavior. The compiler also checks scenarios at compile time to make sure that the contravariant assignment makes sense. For example, the compiler will catch this operation and a compiler error will occur:

    MyAction<Employee> printEmployee = (employee) => Console.WriteLine(employee);
    MyAction<Customer> printCustomer = printEmployee;

    An Employee is not a Customer.

    There is a lot of information, but the best source of information including some of the math behind variance can be found on Charlie Calvert’s and Eric Lippert’s blogs. Dozens of other posts talk about assignment compatibility for the most part—but if they all said just use the out modifier for covariance and the in modifier for contravariance then they’d be a lot farther ahead.

    Variance comes into play in delegates, collections, interfaces, and generics. For the most part I think of the subject as better, more intuitive support for assigning child types to parent types and vice versa and it is covariance and contravariance that extend and preserve assignment compatibility in ways that seem more intuitive.

  • Extending DevExpress Controls and Components

         

    In response to my blog post “Using the AlertControl as an End-User Guidance Mechanism” Robert Fuchs wrote: “Many customers are asking for a sound when the alter form pops up. What best practice do you recommend?” Robert further stated that he basically didn’t want the Beep behavior anywhere in his code with the obvious simple choice being in the BeforeFormShow event handler. This is a good question because it implies the broader question: “should I use inheritance to extend DevExpress components and controls?” The short answer is that wouldn’t be my first choice.

    A developer certainly can inherit from a DX control without any immediate technical problems. Referred to as branching inheriting from our controls means you have to take ownership of the new control you create. The downside is that every time we release a new codebase—which is several times a year—you risk having breaking changes in your one off control. And, there is no need to use inheritance to extend classes, controls, or components. In answer to Richards question, my first choice is use existing events and properties when possible, use extension methods for non-controls and non-component classes, and an IExtenderProvider for controls and components. In direct response to Richard’s query about the AlertControl, add the Beep functionality using an IExtenderProvider. Here is an overview of this aspect of the .NET Framework, how it works, and my solution to Richard’s query.

    If you have a limited book budget buy Martin Fowler’s Refactoring book and Erich Gamma (and company’s) book on Design Patterns. Among many other great things in the Design Patterns book is the Decorator pattern. Decorator is a structural pattern that essentially is used to attach additional responsibilities to an object dynamically. (Checkout dofactory.com for online information about design patterns.) One implementation of it exists in the .NET Framework via the IExtenderProvider interface. IExtenderProvider permits developers to assign new elements to existing components and controls without inheritance. Define a class in a class library that implements IExtenderProvider from the System.ComponentModel namespace. Use the ProvideProperty attribute to define the provided properties and the typeof operator to define what type of control or component the property is provide to. Then, add the code to the IExtenderProvider that supports the provided property, compile the class library, and drag and drop the (extender) component onto a form. For instance, if you implement an IExtenderProvider for the AlertControl then you will need an AlertControl and the extender component on the form. The result is that the extender’s property will appear in the Properties window for the AlertControl not the extender component. Neat. New property without inheritance.

    For our setup we want to add an audible signal to the DevExpress AletrControl without inheriting from the AlertControl. Listing 1 contains the IExtenderProvider that I created in a class library named Extender.

    Listing 1: The MyAlertControlExtender class.

    using System.ComponentModel;
    using Microsoft.VisualBasic;
    using DevExpress.XtraBars.Alerter;
    
    namespace Extender
    {
      [ProvideProperty("BeepOnShow", typeof(AlertControl))]
      [Category("Behavior")]
      public class MyAlertControlExtender : Component, IExtenderProvider
      {
    
        private bool beepOnShow = true;
    
        public bool GetBeepOnShow(AlertControl extendee)
        {
          return beepOnShow;
        }
    
        public void SetBeepOnShow(AlertControl extendee, bool state)
        {
          beepOnShow = state;
          if (state)
            extendee.BeforeFormShow += new AlertFormEventHandler(extendee_BeforeFormShow);
          else
            extendee.BeforeFormShow -= new AlertFormEventHandler(extendee_BeforeFormShow);
        }
    
        void extendee_BeforeFormShow(object sender, AlertFormEventArgs e)
        {
          Interaction.Beep();
        }
    
        #region IExtenderProvider Members
    
        public bool CanExtend(object extendee)
        {
          return extendee is AlertControl;
        }
        #endregion
      }
    }

    In an IExtenderProvider the property extensionss are actually implemented as methods with a Get and Set prefix, as in GetBeepOnShow. In the Visual Studio designer the Get and Set are dropped and the provided property shows up in the Properties window. The extender user in this case will see BeepOnShow as a proprerty of the AlertControl when the MyAlertControlExtender is added to a form with an AlertControl on it. (The extender actually extends every instance of an AlertControl is you have more than one.) If BeepOnShow is true then the delegate is wired to the extended AlterControl’s BeforeFormShow event, and the delegate uses Interaction.Beep for the audible signal. Figure 1 shows the form designer in Visual Studio and the component tray with both an AlertControl and a MyAlertControlExtender and the provided porperty in the Properties window.

    The AlertControl, MyAlertControlExtender and the extended property.
    Figure 1: The AlertControl, the MyAlertControlExtender, and the provided property in the Properties window.

    A fair question might be: why use methods for the provided property in the extender. The answer is that one extender provides the property for all instances of the extended control. Read multiple control support. This means that the provided property has to be distinct for multiple instances of the same type of control, and that the control has to be passed in with the property state, which implies two arguments. Since properties only support one argument a method is necessary. This solution won’t work quite right for more than one AlertControl because there is only one backing field. To support varied BeepOnShow states add a HashTable to the IExtenderProvider, use the hashcode for each individual control to index the HashTable and store the Boolean backing value in the hashtable. Viola! (I left that part as an exercise. If you don’t want to exercise see my earlier post “Validating Using Regular Expressions or an IExtenderProvider” at https://community.devexpress.com/blogs/paulk/archive/2010/03/30/validating-using-regular-expressions-or-an-iextenderprovider.aspx for that part of the solution.)

  • Using the Dynamic Language Runtime to Call IronPython for VS2010

         

    I am a firm believer in symmetry in frameworks and code. If there is a Do behavior then it seems intuitive to have an Undo behavior right alongside of it. Strangely enough there are sort behaviors all over .NET now, so you and I seldom have to write sorts algorithms like quick sort or radix sorts from scratch, but there is no unsort or randomize behavior in the framework. (DevExpress does include Unsorting in some of their controls like the ASPxTreeList, but I couldn’t find an unsort anywhere in the framework itself or help files.) and, occasionally one needs to randomize data.

    Years ago I wrote a Blackjack game that shuffles cards much like—refer to Programming for Fun and Profit – Using the Card.dll at http://www.developer.com/net/net/article.php/3303671/Programming-for-Fun-and-Profit---Using-the-Carddll.htm. An employee, or so she said, from Harrah’s Casino in Biloxi called and asked if they could use the code to put on a CD and provide to gamers as sort of a pillow favor. I said sure. And, this week I was tinkering with some DLR (Dynamic Language Runtime) behaviors looking for cool things to do with Dynamic Objects when I came across Scott Hanselman’s blog post about dynamism—see http://www.hanselman.com/blog/CategoryView.aspx?category=DLR. In this post Scott demonstrated how to load an Iron Python script file and use the DLR to call a shuffle method in that file. Pretty cool.

    Wondering as I often do I wondered what else could be shuffled using this approach. As a result I played with the concept of shuffle “Card” objects as well as arrays of data like the Employees table data in the Northwind database. Here is the sample and the steps to reproduce the results—the code is provided in Listing 1.

    1. Download IronPython 2.6 for,NET 4.0. (You can find the installer at http://ironpython.codeplex.com/releases/view/36280)
    2. After IronPython installs add references to a C# project (or a VB project) for the DLLs in the installation folder, including IronPython.dll, IronPythonModules.dll, Microsoft.Dynamic.dll, Microsoft.Scripting.Debugging.dll, and Microsoft.Scripting.dll
    3. Add using statements for IronPython.hosting ad Microsoft.Scripting.Hosting; these namespaces will be used to load and call into the an IronPython script file
    4. The code will set the current directory to the “IronPython 2.6 for .NET 4.0\Lib” folder and load random.py (an IronPython script) and supporting files.
    5. Finally, the code will use ADO.NET to load data from the Employees table and a dynamic object to invoke random.py’s shuffle method, passing in some of the employee data

    Listing 1: Using IronPython through a dynamic object to shuffle an array of employee names.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Dynamic;
    using IronPython.Hosting;
    using Microsoft.Scripting.Hosting;
    using System.IO;
    using System.Data;
    using System.Data.SqlClient;
    
    namespace DlrIronPyhonShuffleDemo
    {
      class Program
      {
        static void Main(string[] args)
        {
          Directory.SetCurrentDirectory( 
            "C:\\Program Files\\IronPython 2.6 for .NET 4.0\\Lib");
    
          ScriptRuntime p = Python.CreateRuntime();
          dynamic random = p.UseFile("random.py");
    
          const string connectionString = 
            @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True;" +
    "
    MultipleActiveResultSets=True"; DataTable employees = new DataTable(); using(SqlConnection connection = new SqlConnection(connectionString)) { const string sql = "SELECT * FROM Employees ORDER BY LastName"; connection.Open(); SqlCommand command = new SqlCommand(sql, connection); SqlDataAdapter adapter = new SqlDataAdapter(command); adapter.Fill(employees); } var shortEmployees = from row in employees.AsEnumerable() select new {First=row.Field<string>("FirstName"), Last=row.Field<string>("LastName")}; var data = shortEmployees.ToArray(); random.shuffle(data); Array.ForEach(data, (se)=>Console.WriteLine(se)); Console.ReadLine(); } } }

    The ScriptRuntime object contains a script runtime for IronPython for .NET 4.0. The middle of the code is boilerplate ADO.NET code. The LINQ query creates a projection containing just employee first and last names. The IronPython file’s shuffle method threw an exception on the enumerable collection of employee data, but handled an array just fine. (It also handled an array of Card objects—a complex type—in another demo I tinkered with.) the code wraps up by shuffling the array of data and writing the contents to the Console.

    I am not sure how performant such calls will be, but it may not always matter. It might be interesting to write a randomizer and compare shuffling results with managed .NET code versus a dynamic object call. (By the way dynamic random demonstrates how to use a dynamic object in C#; declare the type as Object in VB.) However, if performance isn’t an issue and you can find a solution in another dynamic language like IronPython calling an external method even in script may be useful. As I have stated in other posts, everything dynamic objects and the DLR might be used for probably haven’t been discovered yet, but the DLR is a clever and interesting addition to .NET programming.

  • Central Ohio Day of .NET June 5th 2010 in Wilmington, OH

         

    I will be in Wilmington, OH tomorrow manning the DevExpress booth for the Central Ohio Day of .NET. I won’t be presenting at this DODN day, but will have some great DevExpress swag, some free copies of Professional DevExpress ASP.NET Controls books to give away and would like to hear your early experiences with DevExpress newest release 10.1 for VS 2010.

    Here is the website if you are still on the fence and would like to see more information about speakers, sessions, and sponsors: http://cinnug.org/cododn/.

  • Using the AlertControl as an End-User Guidance Mechanism

         

    For a three year period I was a Military Policeman, for a year I sold mutual funds, and I was a teacher for three years. But, when I found computers nothing else seemed nearly as interesting. When I started consulting early on I combined computers with teaching and was fortunate enough to present on software development and programming in some pretty cool places. One of my favorite places to go was New York City, but one of the funnest places was Dole in Bakersfield. After 8 hours of Delphi training about 20 of us would play Descent. Three dimension spaceship flying, blasting each other for hours, it was a hoot. “Welcome to Valhalla Tower Material Defender!”

    As a partial result of teaching and programming I recognize that teaching end users how to use software is a real challenge and often a big chore for successful software projects. And as a result I look for ways to convey how software works. There are some neat tools that can be used. Right here in Okemos we have TechSmith; they make the great Camtasia product. Of course, you can also incorporate self-training aids right in your software. Now if you wanted to make visual aids for your entire application it is a big chore. You could do it, or you could just add cues for critical or hard to use features. A simple way to do this is DevExpress AlertControl. The AlertControl contains a header and a clickable link message. Respond to the click event and the software can do anything at that point. (What would be really cool is to combine a recoding mechanism—that captured a user’s activities and then can be played back. I did something like that for a Lucent technologies Hoteling application. Hoteling is basically moving someone’s phone settings to a new location, for example for temporary relocated office workers. That application basically had a macro language that supported record and playback modes.)

    In this blog example when “the user” inserts an HTML table into the RichEditControl AlertControl’s are used to walk the end user through insert nested tables and content. The code is not especially pretty—see Listing 1—because it is task-based but with a little ingenuity it could be prettified and perhaps even made to be recordable—do XYZ and playback the procedure through alerts.

    Listing 1: Using the AlertControl to spoon feed users through more advanced capabilities.

    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.XtraBars.Alerter;
    using DevExpress.XtraRichEdit.Commands;
    using DevExpress.XtraBars.Commands.Internal;
    using DevExpress.Utils.Commands;
    using DevExpress.XtraEditors;
    using DevExpress.XtraRichEdit;
    using DevExpress.XtraRichEdit.Commands.Internal;
    using DevExpress.XtraRichEdit.API.Native;
    
    
    namespace BlogDemo
    {
      public partial class Form1 : XtraForm
      {
        
        private bool insertingTable = false;
    
        public Form1()
        {
          InitializeComponent();
        }
    
        private void richEditControl1_HtmlTextChanged(object sender, EventArgs e)
        {
          // If the HTML changed after the insertTableItem click happened
          // then we'll assume that an HTML table was inserted
          const string text = 
            "Edit the content of the table directly and/or add nested tables - show me";
          if(insertingTable)
          {
            insertingTable = false;
            alertControl1.AlertClick += (object s, AlertClickEventArgs ev)=>
              { ShowMe(); };
    
            alertControl1.FormLocation = AlertFormLocation.BottomRight;
            alertControl1.Show(this, new AlertInfo("HTML Table Inserted", text));
            
          }
        }
    
        private void ShowMe()
        {
          using(Form form = new Form())
          {
            RichEditControl control = new RichEditControl();
            control.Parent = form;
            control.Dock = DockStyle.Fill;
            form.TopMost = true;
    
            form.Shown += (object s, EventArgs ev) =>
            {
              const string message1 = "1. Table Inserted with InsertTableCoreCommand
    "; 
              const string message2 = "2. Scroll to start of document
    "; 
              const string message3 = "3. Insert nested table
    "; 
              const string message4 = "4. Insert text in data cell
    ";
              InsertTableCoreCommand command = null;
    
              AlertControl alert4 = new AlertControl();
              alert4.FormLocation = AlertFormLocation.TopLeft;
    
              alert4.AlertClick += (object s4, AlertClickEventArgs e4) =>
              {
                // insert text
                InsertTextCommand text = new InsertTextCommand(control);
                text.Text = "Some text in a table cell";
                text.Execute();
                System.Threading.Thread.Sleep(3000);
                form.Close();
              };
    
    
              AlertControl alert3 = new AlertControl();
              alert3.FormLocation = AlertFormLocation.TopLeft;
              alert3.AlertClick += (object s2, AlertClickEventArgs e2) =>
              {
                // insert nested table
                command.ColumnCount = 4;
                command.RowCount = 4;
                command.Execute();
                alert4.Show(form, new AlertInfo("Insert Text", message4));
              };
    
              
    
              AlertControl alert2 = new AlertControl();
              alert2.FormLocation = AlertFormLocation.TopLeft;
              alert2.AlertClick += (object s2, AlertClickEventArgs e2) =>
              {
                // move to start
                StartOfDocumentCommand start = new StartOfDocumentCommand(control);
                start.Execute();
                alert3.Show(form, new AlertInfo("Insert Nested Table", message3));
              };      
              
    
              AlertControl alert1 = new AlertControl();
              alert1.FormLocation = AlertFormLocation.TopLeft;
    
              alert1.AlertClick += (object s1, AlertClickEventArgs e1)=>
                {
                  // insert the first table
                  command = new InsertTableCoreCommand(control);
                  command.ColumnCount = 5;
                  command.RowCount = 4;
                  command.Execute();
                  alert2.Show(form, new AlertInfo("Scroll", message2));
    
                };
    
              alert1.Show(form, new AlertInfo("Insert Table", message1));
            };
    
            form.ShowDialog();
          }
        }
        
        private void insertTableItem1_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
        {
          insertingTable = true;
        }
      }
    }

    The sample application is simple to build. Add a RibbonControl to a Windows Form. Add a RichEditControl too. Use the RichEditControl’s smart tags menu to create the File bar and Insert bars—see Figure 1. When the user clicks the Table button item the alter is displayed. By clicking the alert’s text the “additional features” tutorial is initiated in response to the RichEditControl’s HtmlTextChanged event. (If the HTML changes then we assume the user inserted a table by clicking OK on the built-in InsertTableCommand’s GUI.)

    HtmlTextChanged checks a local flag. If true—an HTML table was inserted into the RichEditControl—and the initial alert in Figure 1 is shown. AlertClick uses a Lambda expression (as an anonymous delegate) to invoke the ShowMe method. ShowMe runs through a sequence of additional alerts. ShowMe dynamically creates a new form with a RichEditControl. form.Shown uses another Lambda delegate to sequence the additional alerts. When ShowDialog is called the Shown event fires. InsertTableCoreCommand from DevExpress.XtraRichEdit.Commands.Internal inserts a dynamic HTML table—see alert1. alert1 displays alert2. Click alert2’s message and the cursor is moved to the start of the HTML document which happens to coincide with the first table cell in this example; alert2 displays alert3. The AlertControl alert3 reuses InsertTableCoreCommand to insert a nested table, and initiates alert4. The AlertControl alert4 inserts some text in the table cell. Notice that the command are constructed in reverse order with the first alter, alert1, being listed last but actually displayed first.

    Click the AlertControl's text message to initiate an action
    Figure 1: Click the link in the alert control and respond to the event to initiate some further action.

    Is this a real scenario? I think for some projects it might work out pretty good. In practice I would make the self-tutorial feature something that could be disabled. I would probably add a recording mechanism that stream steps to XML and replayed them perhaps rather than code them, but for some projects it might be a requirement. I would be interested if any of you are using the AlertControl—as a tutorial device—in this manner. Of course, you use the AlertControl to just inform users when an operation has completed, if further steps are required, or if there are further optional operations that are available.

    Think of the AlertControl as a viable opportunity to a ToolTip or SuperTip, when you want to force a message to be displayed to the user. Of course, there is some overlap. You can accomplish some of the same kinds of things with the RibbonStatusBar, ToolTips, and popup dialogs, too.

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.