Take a Closer Look at the DevExpress Web Report Designer

Don Wibier's Blog
15 April 2016

image

In this webinar Mehul and I showed you how to use the End User Web Report Designer where we talked about some topics you will run across when implementing the Report Designer in your own application.

Introduction

The Web Report Designer is a very powerful control allowing your end-users to create or customize reports inside their web-browser. When we started with the Reporting Suite, the only way to create reports was by using the designer inside VisualStudio. The report is created as a C# (or VB) class:

image

When we introduced the End User Report Designer, first for WinForms, next for WebForms and currently our WPF Designer is in Release Preview, there where a couple of things we had to take care of:

  • How do we allow end-users to modify a class based report (which is compiled) ?
  • How do we deal with different types of datasources and the metadata we need for field binding etc. during designing?

For the web designer, we had some additional things to cover:

  • How do we enable a design surface in the web browser including as much features as the desktop and Visual Studio versions?
  • What do we do with opening and storing report definitions from and to the server?

Modifying a class based report

What is actually happening within any of the End User Report Designers is that you will need to instantiate a report class and pass it into the OpenReport method of the designer.

   1: var rpt = new DevExpress.XtraReports.UI.XtraReport();
   2: ASPxReportDesigner1.OpenReport(rpt);

In the above example I instantiate a new instance of an empty report. This could instead be one of the report classes which was created with the Visual Studio designer instead.

Once you start modifying this report, those modifications need to be stored which is done in a .repx file. This file is an XML-based file which contains all changes to properties of the base report class as well as all reporting controls which were added while designing like XtraLabels, XtraImages and more.

Dealing with different types of datasources

Because the core reporting engine is used across al the technologies we support (WinForms, WebForms and WPF), this means that you can create a report definition on WinForms and use it on WebForms. This explains why we have special reporting controls like XtraLabel, XtraImage etc. Those controls have no knowledge of the rendering mechanism being used to preview a report. Because of the exact same reason, we have setup a low-level data access library for accessing al kinds of data sources. For example, normally, you cannot use WebForms SqlDatasource on WinForms project.

This DevExpress DataAccess library is also the foundation of our own Object Relational Mapping tool; eXpress Persistent Objects (XPO).

The datasources which come with this library all know how to handover the metadata (field types and names), to the designer to get the best user experience.

The Report design surface in a browser?

The Web Report Designer has a great end-user experience. This is due to the fact that it is a HTML5 / JavaScript based control which is depending on a number of well-known JavaScript libraries like: jQuery, jQuery-UI, KnockoutJS and Globalize.

By default we will submit the JavaScript libraries automatically because of a web.config setting embedRequiredClientLibraries which defaults to true:

   1: <devExpress>
   2:     <settings doctypeMode="Html5" rightToLeft="false" embedRequiredClientLibraries="true" ieCompatibilityVersion="edge" />

If you are adding the designer to an existing project, you might want to check whether you are already using one or more of these libraries, and decide to reference the other ones manually as well. This will help you solve version conflicts and version specific features.

If you need to reference the JavaScript files yourself, please switch the value to false.

Real World Scenario

In a typical development cycle where the Report Designer needs to be implemented, we could assume that the developer will create an initial (class based) report by using the DevExpress Reporting Wizard in Visual Studio.

This Wizard asks a couple of questions like what kind of datasource the report will work on:

image

When using the Wizard, the appropriate DevExpress DataAccess datasource objects are being created and configured so all meta-data will be available. You do not need to worry about that. This will cover most of the data access techniques used today.

Once a basic report has been created by code, this can serve as a base for new reports created by the End User Report Designer:

   1: var rpt = new MyReportCreatedByCode();
   2: ASPxReportDesigner1.OpenReport(rpt);

Adding Datasources in the designer

If you follow the steps above, and start start designing a report in your application, you will notice that things work right out of the box. This is because the wizard makes sure to use the datasource classes in the DevExpress DataAccess library.

If you would create a new report by using an instance of the XtraReport type, or a base report class with no datasources configured, you will need to add them manually to the designer.

The best way is to work with the DevExpress DataAccess connection classes as well:

   1: protected void Page_Load(object sender, EventArgs e)
   2: {
   3:     if (!IsPostBack)
   4:     {
   5:         DevExpress.DataAccess.Sql.SqlDataSource sql = GenerateSqlDataSource();
   6:         ASPxReportDesigner1.DataSources.Add("MS-SQLDatasource", sql);
   7:  
   8:         DevExpress.DataAccess.ObjectBinding.ObjectDataSource objds = GenerateObjectDataSource();
   9:         ASPxReportDesigner1.DataSources.Add("MyObjectDatasource", objds);
  10:  
  11:         DevExpress.DataAccess.EntityFramework.EFDataSource efds = new EFDataSource(new EFConnectionParameters(typeof(ChinookModel)));
  12:         ASPxReportDesigner1.DataSources.Add("MyEFDatasource", efds);
  13:  
  14:         var rpt = new DevExpress.XtraReports.UI.XtraReport();
  15:         ASPxReportDesigner1.OpenReport(rpt);
  16:     }
  17: }
  18: private DevExpress.DataAccess.Sql.SqlDataSource GenerateSqlDataSource()
  19: {
  20:     DevExpress.DataAccess.Sql.SqlDataSource result = new DevExpress.DataAccess.Sql.SqlDataSource("ChinookConnection");
  21:     // Create an SQL query.
  22:     result.Queries.Add(new CustomSqlQuery("MyGenreQuery", "SELECT * FROM Genre;"));
  23:     result.RebuildResultSchema();
  24:     return result;
  25: }
  26:  
  27: private DevExpress.DataAccess.ObjectBinding.ObjectDataSource GenerateObjectDataSource()
  28: {
  29:     DevExpress.DataAccess.ObjectBinding.ObjectDataSource result = new DevExpress.DataAccess.ObjectBinding.ObjectDataSource();
  30:     result.Name = "ObjSource";
  31:     result.DataSourceType = typeof(ItemList);
  32:     result.Constructor = new DevExpress.DataAccess.ObjectBinding.ObjectConstructorInfo();
  33:     return result;
  34: }

The example above illustrates adding three kinds of different datasources.

The first one is an SqlDatasource which can be used if you’re using an SQL based data access technique already. You can see that the SQL statement is specified in the GenerateSqlDataSource method.

The second one is an ObjectDatasource which can be used if you’re using an object based data access technique. In this case the type of the collection needs to be specified as shown in the GenerateObjectDataSource method.

The last one, the EFDatasource, is by far the most easy one and works really well if you are using Entity Framework as data access layer. Here we just need to specify the type of the DbContext class used in Entity Framework and all entities are available in the designer.

Common issues with Datasources

It could happen that in your situation non of the above datasource types work. In that case, you can add your datasources to the End Report Designer. There are some additional things you need to do.

In the webinar I showed that when you just add a ASPNET SqlDatasource to the Designer, things will not work properly. Most important, the designer is unable to determine what fields are available in the Datasource:

image

As you can see Genres1 and Items1 are missing the expand arrows which means that I have tried to expand them. The Report Designer was unable to fetch the meta-data because it doesn’t know how to serialize that to XML and pass it on to the client.

In the example, the most easy way is to change the datasources to a DevExpress.DataAccess.Sql.SqlDatasource and a DevExpress.DataAccess.ObjectBinding.ObjectDataSource because those classes are able to properly serialize without any additional coding. This is what I did during the webinar.

Serialization

image

To get some unsupported datasource to serialize the required information properly to the client, it is necessary to create a custom serializer which is able to serialize your custom datasource.

This is done by creating a class implementing the DevExpress.XtraReports.Native.ISerializer interface.

   1: public class MyDataViewSerializer : DevExpress.XtraReports.Native.IDataSerializer
   2: {
   3:     public const string Name = "MyDataViewSerializer";
   4:  
   5:     public bool CanSerialize(object data, object extensionProvider)
   6:     {
   7:         return (data is DataView);
   8:     }
   9:     public string Serialize(object data, object extensionProvider)
  10:     {
  11:         DataView v = data as DataView;
  12:         if (v != null)
  13:         {
  14:             DataTable tbl = v.ToTable();
  15:             StringBuilder sb = new StringBuilder();
  16:             XmlWriter writer = XmlWriter.Create(sb);
  17:             tbl.WriteXml(writer, XmlWriteMode.WriteSchema);
  18:             return sb.ToString();
  19:         }
  20:         return string.Empty;
  21:     }
  22:     public bool CanDeserialize(string value, string typeName, object extensionProvider)
  23:     {
  24:         return typeName == "System.Data.DataView"; 
  25:     }
  26:     public object Deserialize(string value, string typeName, object extensionProvider)
  27:     {
  28:         DataTable tbl = new DataTable();
  29:         using (XmlReader reader = XmlReader.Create(new StringReader(value)))
  30:         {
  31:             tbl.ReadXml(reader);
  32:         }
  33:         return new DataView(tbl);  
  34:     }
  35: }

After creating this serializer, it should also be registered to the reporting engine which can be done in the Global.asax.

   1: void Application_Start(object sender, EventArgs e)
   2: {
   3:     //...other code...
   4:     SerializationService.RegisterSerializer(MyDataViewSerializer.Name, new MyDataViewSerializer());
   5: }

The last step for using the serializer with a report is to add the serializers Name constant to the report.Extensions dictionary.

   1: var rpt = new DevExpress.XtraReports.UI.XtraReport();
   2: rpt.Extensions[SerializationService.Guid] = MyDataViewSerializer.Name;
   3: ASPxReportDesigner1.OpenReport(rpt);

So depending on the scenario, I would suggest to try and use any of the DevExpress provided datasource classes, and leave the custom serializer as a last resort.

Opening and Saving report definitions from and to the server

Specially with the Web Report Designer, the client (browser) is not aware of how and where the report definitions are stored on the server. This might be on the local (server) file system or in a database or maybe even on some cloud storage.

To support as much storage solutions as possible, the Report Designer is depending on a ReportStorageExtension. You, the developer, need to create such a storage extension and register it with the Report Designer in the Global.asax.

If you start creating such a StorageExtension, you will need to derive this class from the DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension base class and override a  number of methods where you put in your own storage logics.

   1: public class FilesystemReportStorageWebExtension : DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension
   2: {
   3:     public override bool CanSetData(string url)
   4:     {
   5:         //... your implementation ...
   6:     }
   7:  
   8:     public override byte[] GetData(string url)
   9:     {
  10:         //... your implementation ...
  11:     }
  12:  
  13:     public override Dictionary<string, string> GetUrls()
  14:     {
  15:         //... your implementation ...
  16:     }
  17:  
  18:     public override bool IsValidUrl(string url)
  19:     {
  20:         //... your implementation ...
  21:     }
  22:  
  23:     public override void SetData(XtraReport report, string url)
  24:     {
  25:         //... your implementation ...
  26:     }
  27:  
  28:     public override string SetNewData(XtraReport report, string defaultUrl)
  29:     {
  30:         //... your implementation ...
  31:     }
  32:  
  33: }

When you are ready to start using your own storage provider, you need to register it in the Global.asax by using the following method:

   1: void Application_Start(object sender, EventArgs e)
   2: {
   3:     DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(new FilesystemReportStorageWebExtension(this.Context));
   4:     //... other code ...
   5: }

In the webinar, I showed a storage extension which will show an ASPxPopupControl containing an ASPxFileManager and some ASPxButtons and an ASPxTextBox to select a folder on the web-server. The controls in the popup are positioned by an ASPxFormLayout control, and this setup is pretty easy to extract and move it in your own project without modifications.

This demo is included in the demo project on GitHub and also shows you how to perform client operations (like the popup) in combination with the server-side storage extension.

Do note that there are some more files needed for this kind of report management like the JavaScript files in the ~/Scripts folder and the ~/FileDialogControl.ascx. This demo is a good example on how to have the client controls communicate with the server by using an ASPxCallback control.

As soon as you have registered a storage extension, you will also notice that the amount of menu items in the designers menu has increased:

image

Customizing menu-items in the designer

When using a more advanced Storage Extension like the one in the webinar project, there is need to customize the Designers menu as well.

There are a number of things you can do to customize the menu-items:

  • Add custom items with custom behavior
  • Change the behavior of existing items in the menu

Add custom items with custom behavior

Adding menu-items is a pretty straightforward process because of the MenuItems collection property of the Report Designer. This property holds any number of menu-item definitions where you can specify things like Text, JSClickAction, Visible, ImageClassName and some more.

   1: <dx:ASPxReportDesigner ID="ASPxReportDesigner1" runat="server" ClientInstanceName="reportDesigner">        
   2:      <MenuItems>
   3:         <cc:ClientControlsMenuItem JSClickAction="function() {alert('check me');}" Text="Super duper action"  />
   4:     </MenuItems>   
   5: </dx:ASPxReportDesigner>

Change behavior of existing items in the menu

In case of the demo storage extension, I needed to change the default click behaviour of the Open and Save (As) menu-items. For this purpose, we have introduced a ClientSideEvents property CustomizeMenuAction which will be executed when composing the menu.

   1: <dx:ASPxReportDesigner ID="ASPxReportDesigner1" runat="server" ClientInstanceName="reportDesigner">        
   2:     <ClientSideEvents 
   3:         CustomizeMenuActions="reportDesigner_CustomizeMenuActions" /> 
   4: </dx:ASPxReportDesigner>

The JavaScript function for this particular demo looks like this:

   1: function reportDesigner_CustomizeMenuActions(s, e) {
   2:     var defaultOpenAction = e.Actions.filter(function (x) { return x.text === 'Open' && x.imageClassName !== 'reportDesignerIconOpen' })[0];
   3:     var defaultSaveAction = e.Actions.filter(function (x) { return x.text === 'Save' && x.imageClassName !== 'reportDesignerIconSave' })[0];
   4:     var defaultSaveAsAction = e.Actions.filter(function (x) { return x.text === 'Save As' && x.imageClassName !== 'reportDesignerIconSaveAs' })[0];
   5:  
   6:     if (defaultOpenAction)
   7:         defaultOpenAction.clickAction = reportDesigner_Open;
   8:  
   9:     if (defaultSaveAction) {
  10:         defaultSaveClickAction = defaultSaveAction.clickAction;
  11:         defaultSaveAction.clickAction = reportDesigner_Save;
  12:     }
  13:  
  14:     if (defaultSaveAsAction)
  15:         defaultSaveAsAction.clickAction = reportDesigner_SaveAs;
  16: }

What happens here is that I locate the menu-items I want to modify in the e.Actions collection. In this case, we check by text as well as imageClassName to make sure it is the correct one.

If the particular item was found, I simple map the clickAction to the appropriate function.

Github Repo with Demo project

The complete demo create during the webinar containing all subjects in this blog can be cloned or forked at my GitHub repo at: https://github.com/donwibier/DXWebReport

You can also find other projects created during previous webinars.

Accompanied Support Center Tickets

The foundation of the webinar and this blog post where a number of most-viewed support center issues concerning the Web Report Designer.

These tickets with their answers are listed below:

You can find a lot more answers on various topics of the complete DevExpress product line. If you can’t find your answer, just post your question in a new ticket and we will find an answer for you.

no comments
No Comments

Please login or register to post comments.