Raj Kaimal

Rendering an RDLC directly to the Response stream in ASP.NET MVC

The following post shows you how to render an RDLC (Client Report Definition File) in a MVC project. For this tutorial, I am using VS 2008 with MVC 2 Beta. I will also be using the priceless Northwind database and the report will contain a list of customers in the Northwind database.

A sample project zip file is provided at the bottom of this post.

We start off by creating an ASP.NET MVC 2 Empty Web Application.

image 

Add a new ADO.NET entity model and choose the option to “Generate from the database”.

 image
image 
Choose the connection string to use

image

Choose the database objects (Customers for our scenario) and hit finish. We see that a new entity data model has been created in the Models directory.

image 
Create a folder called Content and a subfolder called Reports and add an RDLC into this folder. I hate working with Datasets and hence will not be using the “Report Wizard”. We choose the “Report” template instead. 

image

Create a method that returns all the customers in the database through a Customer partial class and add this in the Model folder.

image

using System;
using System.Linq;
using System.Collections.Generic;

namespace RDLCRendering.Models
{
public partial class Customers
{
public static List<Customers> GetAllCustomers() {
var entities = new NorthwindEntities();
var x = from c in entities.Customers
select c;
return x.ToList();
}
}
}

Open  the RDLC. Your UI should look like so (If you don’t see “Website Data Sources”, click on Data –> Show Data Sources):

image

Design your report by adding a table from the toolbox and dragging and dropping fields from the Website Data Sources (reference).

image 

We will now construct our basic MVC web application. The app will have a home screen and a link to view the PDF report of Northwind customers.
Lets start by adding a Home controller

image 

We add the following code in our HomeController class

namespace RDLCRendering.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
string welcomeMessage = "Welcome to Northwind Traders. Enjoy the view!";
return View((object)welcomeMessage);
}
}
}

Right click on the "Index()” method and select “Add View”.

image

Open the Index view and add code to display the string returned and also add a link for the report in the view like so:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<string>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Index</title>
</head>
<body>
<div>
<h2>
<%=Html.Encode(Model) %></h2>


<%=Html.ActionLink("Customers Report (PDF)", "DetailsReport", "Customers") %>
</div>
</body>
</html>

 
Run the application. You should see a UI similar to the one below:
 
image
If you click on the link you will get a “The resource cannot be found.” error. This is because our Html.Action link refers to a method called “DetailsReport” in a CustomersController which does not exist yet.
Note that we are using the ActionLink overload(linkText, actionName, controllerName).
 
First add a reference to Microsoft.ReportViewer.WebForms in your project.
image
Add a CustomerController class and add a DetailsReport method like so:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using Microsoft.Reporting.WebForms;
using RDLCRendering.Models;

namespace RDLCRendering.Controllers
{
public class CustomersController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult DetailsReport()
{
LocalReport localReport = new LocalReport();
localReport.ReportPath = Server.MapPath("~/Content/Reports/CustomerReport.rdlc");
ReportDataSource reportDataSource = new ReportDataSource("Customers", Customers.GetAllCustomers());

localReport.DataSources.Add(reportDataSource);
string reportType = "PDF";
string mimeType;
string encoding;
string fileNameExtension;

//The DeviceInfo settings should be changed based on the reportType
//http://msdn2.microsoft.com/en-us/library/ms155397.aspx
string deviceInfo =
"<DeviceInfo>" +
" <OutputFormat>PDF</OutputFormat>" +
" <PageWidth>8.5in</PageWidth>" +
" <PageHeight>11in</PageHeight>" +
" <MarginTop>0.5in</MarginTop>" +
" <MarginLeft>1in</MarginLeft>" +
" <MarginRight>1in</MarginRight>" +
" <MarginBottom>0.5in</MarginBottom>" +
"</DeviceInfo>";

Warning[] warnings;
string[] streams;
byte[] renderedBytes;

//Render the report
renderedBytes = localReport.Render(
reportType,
deviceInfo,
out mimeType,
out encoding,
out fileNameExtension,
out streams,
out warnings);
//Response.AddHeader("content-disposition", "attachment; filename=NorthWindCustomers." + fileNameExtension);
return File(renderedBytes, mimeType);
}
}
}

This method will create an instance of the LocalReport class, set the Report path property, add a ReportDataSource which points to the GetAllCustomers method we defined earlier, set the report output type and page dimensions, calls the render method and invokes the File method on the Controller class. This method return a FileContentResult which sends the contents of a binary file to the response stream.
Run the page and click on the link. You should see the PDF report like so:
 
image
If you wish to prompt the user to download the PDF file, uncomment the “content-disposition” line and recompile.
 

Posted by rajbk | 2 comment(s)
Filed under: , , , ,

The Windows Azure Cloud Air Handling Unit

Great technical video presented by Patrick Yantz showing the Azure Cloud Air handling unit. Patrick is a Datacenter Systems Architecture Manager at Microsoft.

Version 1

Version 2

 

Sun Microsystems has something similar – The Sun Modular DataCenter

Posted by rajbk | with no comments
Filed under: , ,

Bing vs Google maps – closing the left panel

You completed a search for a place on both Google and Bing and now wish to close the left panel. Which one is more obvious?

googlemaps       bingmaps

In case you are unable to find it, the action to close the left panel in Bing is in the map itself. You can see it at the bottom right (above Blvd). I was still confused after finding it – will it close the left panel or scroll left?

Posted by rajbk | 1 comment(s)
Filed under: , ,

VS 2010 Beta 2 Installation instructions

For those of you that have VS 2010 Beta 1 installed.

http://go.microsoft.com/fwlink/?LinkID=166199 

http://go.microsoft.com/fwlink/?LinkID=167718 (addendum)

 

Other observations: 

I was prompted for my VS 2010 Beta 1 media when uninstalling it so have your iso or media ready.

VS 2010 Beta 2 prompted for a OS restart during installation.

 See announcement for more details.

Posted by rajbk | with no comments
Filed under: , ,

XMLSerializer and invalid XML

A user had pasted some text from powerpoint into a textarea in one of our web apps which was eventually serialized into XML. Upon trying to de-serialize the XML, the web server threw the exception below:

System.InvalidOperationException: There is an error in XML document (1, 50). ---> System.Xml.XmlException: '♂', hexadecimal value 0x0B, is an invalid character. Line 1, position 50.

The XML that was about to be de-serialized looked like something this:

<?xml version="1.0" encoding="utf-16"?>
<string>Quick&#xB;Brown
Fox
</string>

The code used to de-serialze the xml is shown below:

public static T DeSerializeObject<T>(string xml)
{
    using (System.IO.StringReader sr = new System.IO.StringReader(xml))
    {
        XmlSerializer serializer = new XmlSerializer(typeof(T));
        return (T)serializer.Deserialize(sr);
    }
}


As seen from the exception above, the de-serializer was complaining about the invalid character - &#xB;.  

The PowerPoint slide that text was being pasted from looked something like this:

ppt
The user had used the Shift-Enter key combination to force a line break between text in a bullet. So “Brown” is started on a new line for the same bullet as “Quick”.

By selecting all the text on the slide and pasting it into a hex editor, we see the standard hex values for carriage return and line feed – 0x0D 0x0A between “Brown” and “Fox”. But we see 0x0B was generated for the new line between “Quick” and “Brown”. 0x0B in ASCII stands for a vertical line tab which is an invalid character in XML. 
ppthex

The code used to serialize the string is shown below:

private static string SerializeObject<T>(T source)
{
    var serializer = new XmlSerializer(typeof(T));
 
    using (var sw = new System.IO.StringWriter())
    using (var writer = new XmlTextWriter(sw))
    {
        serializer.Serialize(writer, source);
        return sw.ToString();
    }
}

The problem with this code is that the XmlTextWriter class which inherits from XmlWriter, does not, on its own, validate each character before serializing it.

The recommended way to serialize is to use the static XmlWriter.Create method. The method has overloads where you specify an XmlWriterSettings class. If you do not specify one, the default values for the XmlWriterSettings class is used. One of the properties of this class is the XmlWriterSettings.CheckCharacters Property which is set to true by default. This property ensures that the XmlWriter instance created by the .Create method will perform character checking. By implementing the .Create* method in our code, we ensure that the serializer will throw an exception if it encounters invalid XML.

We, therefore, can rewrite our serializer class like so:

private static string SerializeObject<T>(T source)
{
    var serializer = new XmlSerializer(typeof(T));
    using (var sw = new System.IO.StringWriter())
    using (var writer = XmlWriter.Create(sw))
    {
        serializer.Serialize(writer, source);
        return sw.ToString();
    }
}

* The writer created in this case is of type System.Xml.XmlWellFormedWriter. It will vary based on the .Create overload used.

Posted by rajbk | 5 comment(s)
Filed under: , , ,

LINQ to SQL Paging Gotcha

Framework Version: 3.5

I ran into an issue with how LINQ to SQL implements the Skip and Take operators which leads to records being repeated or missing when performing custom paging.

I am using the Northwind database. My UI will display the City and ContactName of all Customers five records at a time. The LINQ expression projects to an anonymous type with properties of City and ContactName. We see below the LINQ expression, the SQL generated and the data returned for the first and next 5 records.

First 5 Records (Page 1)   Next 5 Records (Page 2)
LINQ Expression
(from c in Customers
        select new {
            c.City,
            c.ContactName
        }).Skip(0).Take(5)
        LINQ Expression
(from c in Customers
        select new {
            c.City,
            c.ContactName
        }).Skip(5).Take(5)
Generated SQL
SELECT TOP (5) [t0].[City], [t0].[ContactName]
FROM [Customers] AS [t0]
  Generated SQL
DECLARE @p0 Int = 5
DECLARE @p1 Int = 5

SELECT [t1].[City], [t1].[ContactName]
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[City], [t0].[ContactName]) AS [ROW_NUMBER], 
    [t0].[City], [t0].[ContactName]
    FROM [Customers] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]
Result set
City            ContactName
--------------- ------------------------------
Berlin          Maria Anders
México D.F.     Ana Trujillo
México D.F.     Antonio Moreno
London          Thomas Hardy
Luleå           Christina Berglund
  Result set
City            ContactName
--------------- ------------------------------
Barquisimeto    Carlos González
Bergamo         Giovanni Rovelli
Berlin          Maria Anders
Bern            Yang Wang
Boise           Jose Pavarotti

From the result set, we see that Maria Anders is repeated in Page 1 and Page 2!  The same behavior can also be observed with a LinqDataSource as defined below:

<asp:GridView ID="GridView1" runat="server" AllowPaging="True" AutoGenerateColumns="False"
    DataSourceID="LinqDataSource1" PageSize="5">
    <Columns>
        <asp:BoundField DataField="City" HeaderText="City" ReadOnly="True" SortExpression="City" />
        <asp:BoundField DataField="ContactName" HeaderText="ContactName" ReadOnly="True"
            SortExpression="ContactName" />
    </Columns>
</asp:GridView>
<asp:LinqDataSource ID="LinqDataSource1" runat="server" ContextTypeName="DataClassesDataContext"
     Select="new (City, ContactName)" TableName="Customers">
</asp:LinqDataSource>


The reason for this behavior is because the SQL generated for Page 1 does not have an order specified which results in the records being returned in an unknown order

SELECT TOP (5) [t0].[City], [t0].[ContactName]
FROM [Customers] AS [t0]

while for subsequent pages, the SQL generated has an order by clause (required for the ROW_NUMBER() function).

(ORDER BY [t0].[City], [t0].[ContactName]) 

While paging through the result set, this will either result in repeating records or missing records*. 

The best way to work around this issue in framework version 3.5 is to always specify an orderby in the expression before the Take and Skip operators. By doing this, you guarantee that the order used for the first page (with the TOP operator) will be the same as the order used for subsequent pages (using the ROW_NUMBER() OVER ORDER function)

(from c in Customers
 orderby c.ContactName
        select new {
            c.City,
            c.ContactName
        }).Skip(0).Take(5)
SELECT TOP (5) [t0].[City], [t0].[ContactName]
FROM [Customers] AS [t0]
ORDER BY [t0].[ContactName]

LINQ To Entities on the other hand forces you to specify an order by in a scenario like this. If you don’t you get the following error:

The method 'Skip' is only supported for sorted input in LINQ to Entities.
The method 'OrderBy' must be called before the method 'Skip'.

This issue seems to have been fixed in the .NET 4.0 framework where the SELECT TOP statement is no longer generated by LINQ TO SQL for the first page.

(from c in Customers
 orderby c.ContactName
        select new {
            c.City,
            c.ContactName
        }).Skip(0).Take(5)
DECLARE @p0 Int = 0
DECLARE @p1 Int = 5
 
SELECT [t1].[City], [t1].[ContactName]
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[ContactName]) AS [ROW_NUMBER], [t0].[City], [t0].[ContactName]
    FROM [Customers] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]

In addition, if you don’t specify an orderby clause or project your result, LINQ To SQL will use all the properties specified in the entity class in the order by clause and select statement leading to ugly and inefficient code:

(from c in Customers
        select c).Skip(10).Take(5)
SELECT [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], 
[t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM (
    SELECT ROW_NUMBER() OVER (ORDER BY [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], 
[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], 
[t0].[Phone], [t0].[Fax]) AS [ROW_NUMBER], [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], 
[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]
    FROM [Customers] AS [t0]
    ) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]



Special thanks to Joseph Albahari for creating LINQPad. 

*  In most cases we get lucky and get the records from SQL based on the PK for the first page (even though as mentioned before, order is not guaranteed) and since our LINQ Expression will most likely have the PK as the first item in the projection it results in the same order for both the first page and subsequent pages. Hence this obscure issue.

Posted by rajbk | 3 comment(s)
Filed under: , ,

Windows 7 Backup

The backup tool that comes with Vista does not allow users to select individual folders for backup. Instead the user is presented with a default list of locations for backup. Because of this limitation, I had to resort to other third party tools to do the job.

vista_backup

With Windows 7, you now have the option of selecting individual folders for backup. Nice!

win7_backup

Posted by rajbk | 2 comment(s)

Windows 7 – Start Bar and Task Bar

I Installed 7 RTM yesterday and I am really  impressed by the speed improvements compared to Windows Vista.

Even though I am still learning the new features, there are a couple of things I have seen that I really like so far.

You can now click on the network icon in the taskbar and easily connect or disconnect to any network you have configured including VPN connections.

vpn_connect

I also like how easy it is to get to your remote desktop connections. You can click on the start/windows icon and get a list of the recent connections made and even pin down your favorites.

 remotedesktop

You can even pin down your favorite programs to the taskbar.

image

You can also easily control which icons appear in the notification area and reduce clutter. For example you can turn off the ASP.net web server notification by clicking on the arrow in the taskbar and selecting customize :

image

notifications

Check out this post for lots more features

Looking awesome so far!

Posted by rajbk | 2 comment(s)

FormView Binding Gotcha

Version: ASP.NET 3.5 SP1

This post describes two gotchas with the FormView control when binding:
1) When performing two way databinding, Null values and Nullables get changed to string.Empty in the FormView.
2) Two way binding is not supported for nested controls.

When performing two way databinding, Null values and Nullables get changed to string.Empty in the FormView.
The way the FormView control handles null values has caused me some grief :-(

Consider a FormView bound to an ObjectDataSource like so:

<asp:FormView ID="FormView1" runat="server" DataSourceID="ObjectDataSource1" DefaultMode="Edit">
<EditItemTemplate>
FirstName:
<asp:TextBox ID="FirstNameTextBox" runat="server" Text='<%# Bind("FirstName") %>' />
<br />
LastName:
<asp:TextBox ID="LastNameTextBox" runat="server" Text='<%# Bind("LastName") %>' />
<br />
<asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update"
Text="Update" />
</EditItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DataObjectTypeName="Employee"
OldValuesParameterFormatString="original_{0}" SelectMethod="GetEmployee" TypeName="EmployeeWrapper"
ConflictDetection="CompareAllValues" UpdateMethod="SetEmployee"></asp:ObjectDataSource>

The Employee object returned by the ObjectDataSource has LastName set to null like so:
[DataObjectMethod(DataObjectMethodType.Select)]
public Employee GetEmployee()
{
return new Employee()
{
FirstName = "Tom",
LastName = null
};
}
 
Click the Update button on the FormView without changing any properties. In the Form ItemUpdating event, we see that both the OldValues and NewValues have the LastName property set to string.Empty.
(OldValue according to FormView)
FirstName : Tom
LastName :

(NewValue according to FormView)
FirstName : Tom
LastName :
On the other hand, if we try the same thing with the DetailsView control, the LastName will still be “null” on update.

This FormView behavior can be problematic when we we need the original object and the modified object to perform an update with conflict detection (see LINQ to SQL example below). Note that ConflictDetection is set to CompareAllValues in the ObjectDataSource.
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" ConflictDetection="CompareAllValues"
DataObjectTypeName="Customer" OldValuesParameterFormatString="original_{0}" SelectMethod="GetCustomer"
TypeName="CustomerWrapper" UpdateMethod="SetCustomer">
<UpdateParameters>
<asp:Parameter Name="customer" Type="Object" />
<asp:Parameter Name="original_customer" Type="Object" />
</UpdateParameters>
</asp:ObjectDataSource>

public void SetCustomer(Customer customer, Customer original_customer)
{
DataClassesDataContext context = new DataClassesDataContext();
context.Customers.Attach(customer, original_customer);
context.SubmitChanges();
}

In brief, LINQ to SQL compares the original value and new value and generates SQL code to update the original object. Since the original object was modified by the FormView, it will never be found by SQL and no update will occur!

While there are other techniques you can use with Linq to SQL to avoid the issue above, the point of this post is to show how the FormView handles null values.

The workaround is to subscribe to the FormView ItemUpdating event and look for string.empty NewValues and set those to null instead.

protected void FormView2_ItemUpdating(object sender, FormViewUpdateEventArgs e)
{
if (e.NewValues["LastName"].Equals(string.Empty))
{
e.NewValues["LastName"] = null;

}
}

In addition, to keep the OldValues unchanged (if you need it for concurrency purposes), set the DataKeyNames property of the FormView to the properties of the object like so:
<asp:FormView ID="FormView2" runat="server" DataSourceID="ObjectDataSource1" OnItemUpdating="FormView2_ItemUpdating"
DefaultMode="Edit" DataKeyNames="LastName, FirstName">

When the Update button is clicked this time, the null values are retained.
(OldValue according to FormView)
FirstName : Tom
LastName : null

(NewValue according to FormView)
FirstName : Tom
LastName : null
 

Two way binding is not supported for nested controls – AFAIK, this is by design.
See an example below:
<EditItemTemplate>
<asp:UpdatePanel runat="server" ID="up1">
<ContentTemplate>
FirstName:
<asp:TextBox ID="FirstNameTextBox" runat="server" Text='<%# Bind("FirstName") %>' />
</ContentTemplate>
</asp:UpdatePanel>
LastName:
<asp:TextBox ID="LastNameTextBox" runat="server" Text='<%# Bind("LastName") %>' />
<br />
<asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update"
Text="Update" />
</EditItemTemplate>

Here we have a TextBox inside an UpdatePanel inside the EditItemTemplate. Since two way binding is not supported for the FirstNameTextBox, the special code for extracting values is not generated resulting in null being sent back for the FirstName property.

To work around this, you will need to subscribe to the ItemUpdating event and extract the values yourself:
protected void FormView2_ItemUpdating(object sender, FormViewUpdateEventArgs e)
{
TextBox FirstNameTextBox = (TextBox)FormView2.FindControl("FirstNameTextBox");
e.NewValues["FirstName"] = FirstNameTextBox.Text;
}

Visibility of controls in a FormView.
Setting Visibility of a control inside a FormView does not work - the TextBox control goes back to visible on update.
protected void Page_Load(object sender, EventArgs e)
{
FormView1.FindControl("FirstNameTextBox").Visible = false;
}

Do it in the DataBound event instead:

protected void FormView2_DataBound(object sender, EventArgs e)
{
FormView1.FindControl("FirstNameTextBox").Visible = false;
}

If you have better ideas on any of the workarounds above, please post a comment or link. Thanks.

Posted by rajbk | 2 comment(s)
Filed under: , ,

Altering Indexed Views - Gotcha

According to BOL, “ALTER VIEW can be applied to indexed views; however, ALTER VIEW unconditionally drops all indexes on the view.”

We can see this behavior by creating an indexed view using the Northwind database. The script is shown below:

CREATE VIEW [dbo].[MyView] WITH SCHEMABINDING
AS
SELECT ContactName, CompanyName
FROM dbo.Customers

GO

CREATE UNIQUE CLUSTERED INDEX [inx_MyIndex] ON [dbo].[MyView]
(
[ContactName] ASC
)
GO

Note that the indexed view must be created with the SCHEMABINDING option. SCHEMABINDING binds the view to the schema of the underlying base tables.

Now the fun part. We alter the index view by adding a new column “ContactTitle”, like so:

ALTER VIEW [dbo].[MyView] WITH SCHEMABINDING
AS
SELECT ContactName, CompanyName, ContactTitle
FROM dbo.Customers

GO

-- Output from Management Studio
-- Command(s) completed successfully.

By looking at the view in management studio, after refreshing the tree, we see that the index is no longer there! I expected to see a warning message when this occurred :-(
 
The easiest way to work around this is to make a copy of the index creation script before you alter the indexed view and run it afterwards.
Posted by rajbk | with no comments
More Posts Next page »