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:
- Create a web browser session to the report page
- Click on the export to CSV button
- this will try to open another link in a new window which we need to suppress!
- Capture the url from the new window
- 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:
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.