对 CSV 报告的 URI 访问

URI Access to a CSV Report

我是C#新手,很新手!我有一个 C# 应用程序,我想从 Reserved.ReportViewerWebControl.axd 下载报告并将其保存到特定位置,我找到了这段代码

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
        }

它有效,但我得到的是源代码而不是 CSV 文件。我知道在普通浏览器中执行报告时得到的 URL 有一个会话 ID 和一个控件 ID。我可以复制 URL 并将其放在 "theURL" 处,然后出现 500 内部服务器错误。我知道我很困惑不知道我需要做什么,但我正在尝试很多事情。这是我最接近的……大声笑我知道很伤心。这是我在浏览器中执行时得到的 URL。

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

我如何通过单击我的应用程序中的按钮下载文件并将其保存在我的位置。

您的目标网页使用 SSRS ReportViewer 控件来管理报告的呈现,此控件在很大程度上依赖于 ASP.Net Session State 来呈现通过调用 Reserved.ReportViewerWebControl.axd 资源处理程序在后台生成报告。

这意味着要使用您已识别的此 axd link,您必须首先触发要在 session 上下文中创建和缓存的内容,然后才能下载它,然后您必须从 same 上下文下载它。

  • 我们不能只 运行 一次页面然后找出 URL,我们必须找到一种方法,在请求之间使用相同的 session 以编程方式执行此操作。

单击下载按钮时,ReportViewer 控件通过 javascript 执行此操作,这意味着没有简单的 link 到 Reserved.ReportViewerWebControl.axd 可以从 html 抓取。 这意味着我们必须手动执行相同的脚本或模拟用户单击 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.

概念比较简单:

  1. 创建网络浏览器session到报告页面
  2. 点击导出为 CSV 按钮
    • 这将尝试在我们需要抑制的新 window 中打开另一个 link!
  3. 从新 window
  4. 捕获 url
  5. 使用相同的 session 上下文下载导出文件
    • 我们不能为此使用 Web 浏览器控件,因为它的界面是 UI 驱动的。

我们不能使用 HttpWebRequestWebClient 对 HTMl DOM 执行 javascript,我们必须使用 Web 浏览器来做到这一点。 出现的另一个问题是我们不能简单地在控件上使用 WebBrowser NewWindowFileDownload 事件,因为这些事件不提供信息,例如新的 [=90] 的 Url =] 或文件下载源或目标。 相反,我们必须引用内部 COM 浏览器(实际上是 IE)并使用本机 NewWindow3 事件将 url 捕获到 Reserved.ReportViewerWebControl.axd,以便我们可以手动下载它。

我使用这些主要参考资料来解释该技术

最后,正如我上面提到的,我们不能使用 Web 浏览器直接从 URL 下载文件,因为它会弹出 SAVE AS 对话框新的网络浏览器或直接保存到配置的下载文件夹。 如参考文章中所述,我们使用 Erika Chinchio 的 GetGlobalCookies 方法,该方法可在@Pedro Leonardo 提供的优秀文章中找到(可用 here

我已将所有这些放入一个简单的控制台应用程序中,您可以 运行,只需将 url 更改为您的报告、导出的标题 link 和保存路径:

以下是我如何获得我想下载的link,具体的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.

这不是我自己想出来的,不知道我是从哪个资源那里学到的,但这意味着其他人有可能得出类似的结论。我实现这个主要是为了让我可以在应用程序的其余部分使用这些概念。但同样在报告方面,我们选择 SSRS 和 RDL 作为报告解决方案的原因之一是它的多功能性,我们编写报告定义,控件允许用户根据需要使用它们。如果我们限制了用户导出报告的能力,那么我们就真的没有充分利用这个框架。