3
votes

I am new to C# and am very green! I have a C# Application that i would like to download a Report from a Reserved.ReportViewerWebControl.axd and save it to a specific location, I found this code

var theURL = "http://TEST/TEST/Pages/TEST.aspx?&FileName=TEST&rs:Command=GetResourceContents";

        WebClient Client = new WebClient
        {
            UseDefaultCredentials = true
        };

        byte[] myDataBuffer = Client.DownloadData(theURL);

        var filename = "test.csv";
        var fileStructureLocal = @"C:\Users\%UserName%\TEST\Downloads".Replace("%UserName%", UserName);
        var fileStructureNetwork = "\\\\TEST\\TEST\\TEST\\TEST";

        var fileLocation = fileStructureLocal + "\\" + filename;

        if (System.IO.File.Exists(fileLocation) == true)
        {
            //DO NOTHING
        }
        else
        {
            System.IO.File.WriteAllBytes(fileLocation, myDataBuffer);
            //File.WriteAllBytes("c:\\temp\\report.pdf", myDataBuffer);
            //SAVE FILE HERE
        }

it works but i get the Source Code and not the CSV file. I know the URL i get when i execute the reports in the normal browser has a session ID and a Control ID. I can copy that URL and put it at "theURL" and i get a 500 internal server error. I know i am all mixed up not sure what i need to do, but am trying many things. This was the closest i got...lol Sad i know. This is the URL I get when i execute it in the Browser.

http://test/test/Reserved.ReportViewerWebControl.axd?%2fReportSession=brhxbx55ngxdhp3zvk5bjmv3&Culture=1033&CultureOverrides=True&UICulture=1033&UICultureOverrides=True&ReportStack=1&ControlID=fa0acf3c777540c5b389d67737b1f866&OpType=Export&FileName=test&ContentDisposition=OnlyHtmlInline&Format=CSV

How would i get this to download the file from a button click in my App and save it in my location.

1
Is this your website? As in are you open to a solution where you make changes to the page hosting the report viewer control to make this easier?Chris Schaller
Can not change the site, i don't have access to it. I was hoping to build a solution to use what is there.Casey Smith

1 Answers

3
votes

Your target web page uses a SSRS ReportViewer control to manage the rendering of the reports, this control relies heavily on ASP.Net Session State to render the report in the background via calls to the Reserved.ReportViewerWebControl.axd resource handler.

This means that to use this axd link that you have identified you must first trigger the content to be created and cached within the session context before it can be downloaded, and then you must download it from the same context.

  • We can't just run the page once and figure out the URL, we have to find a way to do this programatically using the same session between requests.

The ReportViewer control does this via javascript when the download button is clicked, which means there is no simple link to Reserved.ReportViewerWebControl.axd to scrape from the html. This means we have to execute the same script manually or simulate the user clicking the link.

This solution will go into some screen-scraping techniques (UX Automation) to simulate clicking the export button and capturing the result but I would avoid this if you can.

You really should attempt to contact the developer directly for guidance, they may have implemented some simple URL parameters to export directly without having to automate the interface.

The concept is relatively simple:

  1. Create a web browser session to the report page
  2. Click on the export to CSV button
    • this will try to open another link in a new window which we need to suppress!
  3. Capture the url from the new window
  4. Download the export file using the same session context
    • We can't use the web browser control for this, because it's interface is UI driven.

We can't use HttpWebRequest or WebClient to execute the javascript against the HTMl DOM, we have to use a Web Browser to achieve this. The other issue that comes up is that we cannot simply use the WebBrowser NewWindow or FileDownload events on the control as these events do not provide information such as the Url for the new windows or the file download source or target. Instead we must reference the internal COM Browser (effectively IE) and use the native NewWindow3 event to capture the url to Reserved.ReportViewerWebControl.axd so we can download it manually.

I use these main references to explain the technique

Finally, as I mentioned above, we cannot use the Web Browser to directly download the file from the URL as it will popup the SAVE AS dialog in a new web browser or save directly to the configured Downloads folder. As described in the reference article we use the GetGlobalCookies method from Erika Chinchio which can be found in the excellent article provided by @Pedro Leonardo (available here)

I've put all this into a simple console app that you can run, just change the url to your report, the title of the export link and the save path:

The following is how I obtained the link that I wanted to download, the exact link title and composition will vary depending on the implementation:

Locating the export link

class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        SaveReportToDisk("http://localhost:13933/reports/sqlversioninfo", "CSV (comma delimited)", "C:\\temp\\reportDump.csv");
    }

    /// <summary>
    /// Automate clicking on the 'Save As' drop down menu in a report viewer control embedded at the specified URL
    /// </summary>
    /// <param name="sourceURL">URL that the report viewer control is hosted on</param>
    /// <param name="linkTitle">Title of the export option that you want to automate</param>
    /// <param name="savepath">The local path to save to exported report to</param>
    static void SaveReportToDisk(string sourceURL, string linkTitle, string savepath)
    {
        WebBrowser wb = new WebBrowser();
        wb.ScrollBarsEnabled = false;
        wb.ScriptErrorsSuppressed = true;
        wb.Navigate(sourceURL);

        //wait for the page to load
        while (wb.ReadyState != WebBrowserReadyState.Complete) { Application.DoEvents(); }

        // We want to find the Link that is the export to CSV menu item and click it
        // this is the first link on the page that has a title='CSV', modify this search if your link is different.
        // TODO: modify this selection mechanism to suit your needs, the following is very crude
        var exportLink = wb.Document.GetElementsByTagName("a")
                                    .OfType<HtmlElement>()
                                    .FirstOrDefault(x => (x.GetAttribute("title")?.Equals(linkTitle, StringComparison.OrdinalIgnoreCase)).GetValueOrDefault());
        if (exportLink == null)
            throw new NotSupportedException("Url did not resolve to a valid Report Viewer web Document");

        bool fileDownloaded = false;
        // listen for new window, using the COM wrapper so we can capture the url
        (wb.ActiveXInstance as SHDocVw.WebBrowser).NewWindow3 +=
            (ref object ppDisp, ref bool Cancel, uint dwFlags, string bstrUrlContext, string bstrUrl) =>
            {
                Cancel = true; //should block the default browser from opening the link in a new window
                Task.Run(async () =>
                {
                    await DownloadLinkAsync(bstrUrl, savepath);
                    fileDownloaded = true;
                }).Wait();
            };

        // execute the link
        exportLink.InvokeMember("click");

        //wait for the page to refresh
        while (!fileDownloaded) { Application.DoEvents(); }

    }

    private static async Task DownloadLinkAsync(string documentLinkUrl, string savePath)
    {
        var documentLinkUri = new Uri(documentLinkUrl);
        var cookieString = GetGlobalCookies(documentLinkUri.AbsoluteUri);
        var cookieContainer = new CookieContainer();
        using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
        using (var client = new HttpClient(handler) { BaseAddress = documentLinkUri })
        {
            cookieContainer.SetCookies(documentLinkUri, cookieString);
            var response = await client.GetAsync(documentLinkUrl);
            if (response.IsSuccessStatusCode)
            {
                var stream = await response.Content.ReadAsStreamAsync();

                // Response can be saved from Stream
                using (Stream output = File.OpenWrite(savePath))
                {
                    stream.CopyTo(output);
                }
            }
        }
    }

    // from Erika Chinchio which can be found in the excellent article provided by @Pedro Leonardo (available here: http://www.codeproject.com/Tips/659004/Download-of-file-with-open-save-dialog-box),
    [System.Runtime.InteropServices.DllImport("wininet.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
    static extern bool InternetGetCookieEx(string pchURL, string pchCookieName,
System.Text.StringBuilder pchCookieData, ref uint pcchCookieData, int dwFlags, IntPtr lpReserved);

    const int INTERNET_COOKIE_HTTPONLY = 0x00002000;

    private static string GetGlobalCookies(string uri)
    {
        uint uiDataSize = 2048;
        var sbCookieData = new System.Text.StringBuilder((int)uiDataSize);
        if (InternetGetCookieEx(uri, null, sbCookieData, ref uiDataSize,
                INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
            &&
            sbCookieData.Length > 0)
        {
            return sbCookieData.ToString().Replace(";", ",");
        }
        return null;
    }
}

The reason I advise to talk to the developer before going down the screen scraping rabbit hole is that as a standard when I use the report viewer control I always try to implement the SSRS native rc: and rs: URL parameters or atleast make sure I provide a way to export reports directly via url.

you cannot use these parameters out of the box, they are designed to be used when you are querying the SSRS Server directly, which your example does not.

I didn't come up with this on my own, no idea which resource I learnt it from but that means there is a chance others have come up with a similar conclusion. I implement this mainly so I can use these concepts throughout the rest of the application. But also when reports are concerned, one of the reasons we choose SSRS and RDLs as a reporting solution is it's versatility, we write the report definition, the controls allow users to consume them however they need to. If we have limited to ability for the user to export reports, we have really under utilized the framework.