【问题标题】:URI Access to a CSV Report对 CSV 报告的 URI 访问
【发布时间】:2020-02-14 21:06:20
【问题描述】:

我是 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

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

【问题讨论】:

  • 这是您的网站吗?您是否愿意接受一种解决方案,您可以更改托管报表查看器控件的页面以使其更容易?
  • 无法更改站点,我无权访问它。我希望建立一个解决方案来使用现有的东西。

标签: c# reportviewer


【解决方案1】:

您的目标网页使用 SSRS ReportViewer 控件来管理报表的呈现,该控件严重依赖 ASP.Net Session State 通过调用 @987654327 在后台呈现报表@资源处理程序。

这意味着要使用您已识别的这个axd 链接,您必须首先触发要在会话上下文中创建和缓存的内容,然后才能下载它,然后您必须从相同的地方下载它 上下文。

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

当单击下载按钮时,ReportViewer 控件通过 javascript 执行此操作,这意味着没有指向 Reserved.ReportViewerWebControl.axd 的简单链接可以从 html 中抓取。 这意味着我们必须手动执行相同的脚本或模拟用户点击链接。

此解决方案将使用一些屏幕抓取技术(UX 自动化)来模拟单击导出按钮并捕获结果,但如果可以的话,我会避免这种情况。

您确实应该尝试直接联系开发人员寻求指导,他们可能已经实现了一些简单的 URL 参数来直接导出,而无需自动化界面。

概念比较简单:

  1. 创建到报告页面的 Web 浏览器会话
  2. 单击导出为 CSV 按钮
    • 这将尝试在新窗口中打开另一个链接,我们需要禁止该链接!
  3. 从新窗口抓取网址
  4. 使用相同的会话上下文下载导出文件
    • 我们不能为此使用 Web 浏览器控件,因为它的界面是 UI 驱动的。

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

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

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

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

以下是我如何获得我要下载的链接,具体的链接标题和组成会因实现而异:

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;
    }
}

我建议在进入屏幕抓取兔子洞之前与开发人员交谈的原因是,作为标准,当我使用报表查看器控件时,我总是尝试实现 SSRS native rc: and rs: URL parameters 或至少确保我提供了一种方法直接通过 url 导出报告。

您不能直接使用这些参数,它们被设计为在您直接查询 SSRS 服务器时使用,而您的示例没有这样做。

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

【讨论】:

  • 惊人的解释!!非常感谢您,在走这条路之前,我会听取您的建议并与网站开发人员交谈。但是该死,你很好。
  • 我昨天对该解决方案进行了相当多的测试,它比我最初预期的更可靠,但可靠性取决于托管站点和GetGlobalCookies PInvoke。它不会在服务上下文或非 win32 环境中按原样工作。
猜你喜欢
  • 1970-01-01
  • 2012-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-29
  • 1970-01-01
  • 1970-01-01
  • 2010-09-26
相关资源
最近更新 更多