Thursday 28 April 2011

SSRS 2008 Release 2 Update 5

I said in my last post that after updating my SSRS R2 version to update 5, I started getting problems when running reports from the Reports Manager. In particular, I got the following JavaScript error:

"Type Microsoft.Reporting.WebFormsClient.ReportViewer has already been registered. The type may be defined multiple times or the script file that defines it may have already been loaded."

Once this error has occurred, there's a knock on effect which causes a few more JavaScript errors and the final result is that any parameters that need to be entered, can't. All the parameter fields are disabled.

After a bit of research, I found out the cause of this problem and I found a workaround. I'm now going to go through how I found out what caused the problem and suggest the fix I've implemented. If you want to skip how I found out what caused the problem then the source code can be found here.

So, going by the error, it seems that the JavaScript type Microsoft.Reporting.WebFormsClient.ReportViewer has been defined twice, so, it makes sense that that's where we should start. Let's try and find the script(s) that define that type.

I'm using Microsoft Internet Explorer 9 which includes a "Developer Tools" feature. This feature now includes a "Network" tab which, when capturing, will show which files are requested by the browser, how those files are retrieved (POST, GET etc.) and the header and body of each individual request and subsequent response. Using this feature when we click on the report, we get something that looks like this:




I've highlighted the two rows that look interesting, just by looking at the name of the resource, it seems like these two could be useful. So, if we look at the response body of those two requests, we find the following: ReportViewer.js and ViewerScript.

If you open those, and search for "Microsoft.Reporting.WebFormsClient.ReportViewer" you'll notice that they both create that type. As ReportViewer.js is the first to load, that creates it first, then ViewerScript runs and it tries to create it again. An exception is thrown and then we start getting all our problems.

So, how do we fix this? Well, if we look closely at the address, we'll see that it requests the file Reserved.ReportViewerWebControl.axd. Axd? That seems like an odd file extension. Well, it is, it's normally used for ASP.NET HTTP Handlers and this is no different. If you check the web.config file for the Report Manager, you'll find this:


<httpHandlers>
      <add verb="*" path="Reserved.ReportViewerWebControl.axd" type="Microsoft.Reporting.WebForms.HttpHandler, ReportingServicesWebUserInterface, Version=10.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />      
      <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false" />
</httpHandlers>


The first add tag clearly maps the path Reserved.ReportViewerWebControl.axd to the type Microsoft.Reporting.WebForms.HttpHandler. For those of you that haven't come across HttpHandlers before, they're a nifty way of requesting resources that have been compiled into a DLL. The class, in this case, Microsoft.Reporting.WebForms.HttpHandler has to implement the IHttpHandler interface which defines a ProcessRequest method, this method allows you to return anything you want back to the browser, it could be a resource from a DLL or it could be some text that's generated programatically on the fly. As long as there's a mapping for it within the httpHandlers section of your web.config then every time a request is made to the path defined then instead of looking for the physical file on your web server, it'll call the class instead. I've skimmed over http handlers in a hope that you kind of understand their purpose but for more information, see here: http://msdn.microsoft.com/en-us/library/bb398986.aspx#Features

So, how can we use this knowledge to fix our little problem? Well, we know the contents of the JavaScript files that are causing the problem. In particular, we know it's the type defined in the ViewerScript that's being problematic. So, if we make a copy of that file but remove the Microsoft.Reporting.WebFormsClient.ReportViewer type from it, then, if we change the http handler mapping to point to a http handler created by us that, when asked for ViewerScript, returns our modified ViewerScript, rather than the original, then we might just have a fix.

It's not quite that easy though. The path defined within the web.config can only contain just that, the path, it cannot contain any of the query string, so, we'll need our http handler to function just as the original did apart from when requesting the ViewerScript. Well, we can do this pretty easily by wrapping the original within side our own. To very quickly walk you through this, if you create a new class library project, then add a new Resource.resx file to the project. Then, create a new JavaScript file and called it ViewerScript.js. Copy the contents of the original ViewerScript but without the Microsoft.Reporting.WebFormsClient.ReportViewer into the new file. Then, add the file to Resource.resx. Finally, you need to add a reference to the ReportingServicesWebUserInterface DLL which you can pull directly out of the Report Manager bin directory. This will give you access to the original HttpHandler that Microsoft used.

If all that's been set up correctly, then you can create a new class and copy and paste the following:


using System.Web;

namespace SSRSR2
{
    public class HttpHandler : IHttpHandler
    {
        private Microsoft.Reporting.WebForms.HttpHandler handler;
        public HttpHandler()
        {
            handler = new Microsoft.Reporting.WebForms.HttpHandler();
        }

        public void ProcessRequest(HttpContext context)
        {
            if (context.Request.QueryString["Name"] != null && context.Request.QueryString["Name"] == "ViewerScript")
            {
                context.Response.Write(JsHandlerFix.Resource.ViewerScript);
                context.Response.End();
            }
            else
            {
                handler.ProcessRequest(context);
            }
        }

        public bool IsReusable
        {
            get { return handler.IsReusable; }
        }
    }
}


Essentially, this just creates a new instance of the original http handler, if the name of the resource requested isn't "ViewerScript" then it just passes the request on to the original handler and lets that deal with it, which will ensure that the functionality between the original handler and our handler is exactly the same. Then, when "ViewerScript" is requested, we grab our modified ViewerScript from our Resource file and return it.

With all that done, if you compile everything and place the resulting DLL into the bin directory of the Reports Manager, then, all we have to do is change the http handler mapping within the web.config and we should be good to go.

Assuming the name of your DLL is "ViewerFix" then your http handler tag within the web.config file should look like this:


<httpHandlers>
      <add verb="*" path="Reserved.ReportViewerWebControl.axd" type="SSRSR2.HttpHandler, ViewerFix" />
      <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false" />
</httpHandlers>


And that's it. Restart your reports server service and you should now be able to change the parameters within the reports manager so you can actually run reports.

Just incase you didn't follow the above, all the source code can be found here, feel free to use it as you please.

Now I imagine this error isn't something that'll be around for long, it's too big an error for Microsoft not to address it quickly but, if you need a solution now and can't hang around, here you go.

Enjoy!

Sunday 24 April 2011

SSRS 2008 - Enabling Forms Authentication with ActiveDirectoryMembershipProvider

SQL Server Reporting Services is a very powerful reporting tool however, by default, it uses Windows Authentication (NTLM) to authenticate users. This is fine if you're running over an intranet but if you're not then you need to do a little bit of work to enable forms authentication.

Fortunately, SSRS does support forms authentication in the form of a Security Extension. To give you some background, SSRS allows you, the developer, to extend it's functionality by the means of extensions. There are four main extension types:

  1. Data Processing Extension - This allows you to define how to access data from a specific data source type not currently supported by SSRS.
  2. Delivery Extension - Once a report has been generated it can be "delivered" to various locations, for example, it can be sent to someone via an e-mail address. This allows you to code for locations not currently supported by SSRS.
  3. Rendering Extension - Currently, you can render a SSRS report in a wide array of formats, within a PDF or HTML are just two for example. A rendering extension allows you to extend this to support even more formats that aren't supported by SSRS.
  4. Security Extension - This is the one we're interested in here. Security extensions allow you to precisely define how a user is authenticated and what permissions that user has. By default, this is set to work with Window authentication.
With the ability to write our own security extension, we can set up SSRS to use forms authentication rather than Windows. Fortunately, Microsoft provide a very good example of how to do this and that can be found here: http://msdn.microsoft.com/en-us/library/aa902691(SQL.80).aspx

This sample however, does not tell you how to use a MembershipProvider, in particular, I imagine the one most people will want to use is the ActiveDirectoryMembershipProvider which will validate the username and password provided by a user with an ActiveDirectory membership store. In my particular case, I want to be able to validate against an ActiveDirectory store but I also want to perform just a little bit more validation, so, I'm going to extend the ActiveDirectoryMembershipProvider to achieve this.

Fortunately, this is all very simple to set up. As all authentication is done via a web service, you, as a developer, can treat it as its own web application and so, with a couple of entries within the report server's web.config file, a couple of lines of code within your security extension and a class that extends the ActiveDirectoryMembershipProvider, then you're good to go. Here, I explain the changes required. 

First off, lets change the web.config file. We need to create a connection string that'll link to our ActiveDirectory store, to do this, just above the system.web tag, we need to add the following:


<connectionStrings>
    <add name="ADConnectionString" connectionString="LDAP://SERVERNAME:389" />
</connectionStrings>


Following on from the Microsoft sample, you should have changed the authentication tag to look something like this:


<authentication mode="Forms" >
<forms loginUrl="logon.aspx" name="sqlAuthCookie" timeout="60" slidingExpiration="true" path="/" />
</authentication>
<authorization> 
    <deny users="?" />
</authorization>


Under this tag, you'll need to add the following:


 <membership defaultProvider="MembershipADProvider">
    <providers>
        <add
          name="MembershipADProvider"
          type="MyNamespace.CustomADMembershipProvider, CustomADMembershipProvider"
          connectionStringName="ADConnectionString
          connectionUsername="DOMAIN\admin
          connectionPassword="Password"
          enableSearchMethods="true"
          attributeMapUsername="sAMAccountName"
          connectionProtection="None"/>
    </providers>
</membership>


Obviously, you'll need to change the connection username and password to be the admin username and password which will have the correct permissions to be able to read from the membership store.
That will register the membership provider with the web application so that we can then access it from code.

You'll notice I've changed the type to relate to our custom membership provider which we've yet to write. Lets do that now...


namespace MyNamespace 
{
    public class CustomADMembershipProvider : ActiveDirectoryMembershipProvider
    {
         public override bool ValidateUser(string username, string password)
        {
            bool isValid = base.ValidateUser(username, password);
            if (isValid)
            {
                // Extra validation, for example, may we don't want anyone with the username
                // of "BadUser" to have access.
                if(username.ToUpper() == "BADUSER"){
                    isValid = false;
                }
           }
            return isValid;
        }
    }
}



Just to give you a quick run down of what's going, we're overriding the ActiveDirectoryMembershipProvider so that we can use the base implementation to help us validate against the ActiveDirectory store that we defined within the web.config file. However, we now override the ValidateUser method so we can add in our own custom validation code. The first line of the ValidateUser method just ensures that the user is a valid ActiveDirectory user. If they are then we perform our custom validation, in this code I've given a very simple example of saying that if the username is some form of "BadUser" then we should not allow them access.

If we then compile that class and put the resulting DLL file into the bin directory of the ReportServer directory, it will now be accessible from the ReportServer.

Finally, we need to modify the security extension to use this membership provider. By this stage, I'm assuming you've atleast read over the Microsoft sample on how to enable forms authentication. If so, then you should know what I mean when I say that we need to modify the LogonUser method of the authentication extension. This method is the method that all logons will go through. It doesn't matter how you're logging on to the ReportServer, be it through a web service or through ReportsBuilder, this method will always be hit. We need to modify it so it uses our CustomADMembershipProvider. This is very simple now that we've modified the web.config file and placed our CustomADMembershipProvider DLL within the bin directory of the ReportServer, infact it's so simple, it only requires a single line of code, as shown below.


public bool LogonUser(string userName, string password, string authority)
{
    return Membership.ValidateUser(userName, password);
}


And that's it, in theory, you're good to go. Within SSRS 2008 Release 1, this worked first time, however, in release two, it didn't and I had to install an update (I installed Cumalative Update 5), doing this opened up a few more problems which I had to overcome before I could actually run reports from the Reports Manager. More about that in my next blog!

Sunday 17 April 2011

HTML 5 - Drag and Drop


So, every developer loves looking into new things right? Well I'm no different so with HTML 5 predicted to be the hot new technology on the block, I thought it only right that I take a bit of time to look into it. So, every now and again I'll be posting information regarding what new options HTML 5 will give us and today, I'm going to start with the drag and drop specification.

At the time of writing this, I have three browsers installed on my computer, Internet Explorer 9, Firefox 4 and Google Chrome 11. Drag and drop is only currently supported by two of these, Firefox and Chrome, so if you're not using one of those, none of the demo's provided in this post will work.

So, the HTML 5 specification provides us with a seven new JavaScript events to listen for:
  • dragstart
  • drag
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend
There's also a new property for HTML elements called draggable, just by setting it to true will enable an element to be draggable, for example:



Try dragging me, you should be able to move me around, although you can't drop me anywhere.

So, now we've made an element draggable, we need to be able to drop it somewhere. Using the events above, we can do just that. But first, let me quickly describe what each event is used for.

dragstart
As with most of these events, it does exactly what it says on the tin. This event fires when you very first try attempting to drag the element to which the event is attached. Returning true will enable the drag, returning false won't.

drag
Fires while you're dragging something. Essentially the same as onmousemove but obviously, only fires while you're dragging something.

dragenter
Fires when you first drag an element over the target element to which this event is attached. Return false if the target element is a drop zone.

dragleave
Fires when your mouse leaves the elements to which this event is attached while dragging another element.

dragover
Fires as you drag an element over the target element to which this event is attached. A little oddly, you need to return false if the target element is a drop zone.

drop
Fires when the user releases the mouse button while dragging over the target element that has this event attached, effectively dropping the dragged element.

dragend
Essentially the same as the drop event as in it fires when the user has released the mouse button while dragging the element, except this event is usually placed on the element being dragged rather than the drop zone element.

Now we know what all the events are used for, we can put together a clever combination and come up with a simple demo.

I'm draggable between the two grey boxes.

So, lets look at the HTML for this...

<table border="0" cellpadding="10" cellspacing="10" style="width: 100%;">
  <tbody>
     <tr>
       <td style="text-align: center;" width="50%">
          <div id="zoneOne" ondragenter="return dragEnter(event);" ondragover="return dragOver(event);" ondrop="return dragDrop(event);" style="background-color: grey; height: 100px; padding: 5px; text-align: center; width: 100%;">
              <div id="dragObj" draggable="true" ondragend="return dragEnd(event);" ondragstart="return dragStart(event);" style="background-color: red; margin: 5px; padding: 10px; width: 50%;">
I'm draggable between the two grey boxes.
              </div>
          </div>
       </td>      
       <td width="50%">
           <div id="zoneTwo" ondragenter="return dragEnter(event);" ondragover="return dragOver(event);" ondrop="return dragDrop(event);" style="background-color: grey; height: 100px; padding: 5px; text-align: center; width: 100%;">
           </div>
       </td>   
    </tr>
  </tbody>
</table>

I've highlighted the drag and drop related mark up in red. There's nothing special here, we mark our draggable element by setting the draggable property to true on that element. Then the rest is just event mapping. We map the dragstart and dragend events to the element that we're going to be dragging around screen. We then map the dragenter, dragover and drop events on the drop area elements. So, what do those mappings do?

Here's the code for them:

  function dragEnter(ev){
    return false;
  }
  function dragDrop(ev){
    var idelt = ev.dataTransfer.getData("Text");
    var elem = document.getElementById(idelt);
    ev.target.appendChild(elem);
    ev.stopPropagation();
    return false;
  }
  function dragOver(ev){
    return false;
  }
  function dragStart(ev){
    ev.dataTransfer.effectAllowed='move';
    var id = ev.target.getAttribute('id');
    ev.dataTransfer.setData("Text", id);
    return true;
  }
  function dragEnd(ev){
    ev.dataTransfer.clearData("Text");
    return true;
  }

Now, a quick walkthrough of each function...

  • dragEnter always returns false. There's no conditions on which I don't want the draggable item to be droppable within the area defined.
  • dragOver function does the same as the dragEnter for the same reason.
  • dragStart function sets the effectAllowed property of the dataTransfer object. This defines what the drag and drop event is actually allowed to do, in this case we say we can move it. Then we set the type of data that we want to move, in this case it's just text which we'll make the ID of the element we're dragging around. 
  • drop function then grabs that id defined within dragStart, finds the element with that id and then append its to our drop zone, effectively moving it from one zone to another.
  • dragEnd function just clears out the ID we were storing so it doesn't interfere with any future drag and drop operations.

As you've seen, I've made use of the dataTransfer object. This object only exists within the event object when we're dealing with a drag and drop operation. It essentially stores information regarding the operation as it happens. So, you can set the effectAllowed property which defines what effects are allowed within this drag and drop operation. The getData and setData methods allow us to store information regarding the operation, essentially saving us having to define extra global variables that all the functions need access to. For more information on the dataTransfer object and it's members, take a look here.

Ok, well, that's it for drag and drop. I think the next HTML 5 demo I'll be looking at, which is kind of related, is the HTML 5 File API, which will effectively allow users to be able to drag files from their desktop straight on to your web application and in the process, will upload the file to your web server. I believe this is now supported by Gmail to upload file attachments and it's all very clever stuff. More on that at a later date!