3

I will provide as much information as I can about the project, followed by the relevant source code, followed by information regarding what I have tried already (I will try to include code snippets for what I tried where I have them, in case I did them wrong).

I am pretty sure my problem is related to the serialization/deserialization of the data coming back from the report server, but I will admit that it is entirely possible that I am wrong about that.

I have two separate projects (in Visual Studio 2013). The 'client' project is a WPF application which is attempting to display the contents of a ServerReport using ReportViewer. The 'service' project is a WCF application which is attempting to return the contents of the report to the client project after making the call to Microsoft's ReportServer. The previous version of this software has the client software making the request directly to the report server. The changes I have made are to send the parameters for the request to the service project, which gets the authentication information from our database and makes the call to the report server. The goal is that the client side of our application should not have knowledge of or access to the authentication information, but only of the data.

I am open to any solution to this which accomplishes this goal, even if it is completely different from what I have set up so far.

The application populates a list of available reports from the current user's data. Upon clicking of the 'View' button, the report's details should be displayed using report viewer.

Inside the click event for the view button, the parameters for the report server request are populated prior to the call to RefreshReport(). This code has not been altered and is not affected by the new process.

public partial class CurrentReport : (Our base page object)
{
  public ReportViewer _report;
  private string _reportPath;

  public CurrentReport()
  {
    try
    {
      InitializeComponent();
      _report = new ReportViewer();
      BuildReportViewer();
    }
    catch (Exception ex)
    {
      // Log Exception
    }
  }

  public void BuildReportViewer()
  {
    try
    {
      // wfh is an WindowsFormsHost property which
      //   CurrentReport inherits from its parent
      if (wfh.Child == null)
      {
        _report = new ReportViewer();
        wfh.Child = _report;
    }
    catch (Exception ex)
    {
      // Log Exception
    }
  }

  public bool RefreshReport(string reportPath, List<ReportParameter> parameters = null)
  {
    try
    {
      if ((parameters != null) && (!String.IsNullOrEmpty(reportPath)))
      {
        // Parameters passed to this method are of the type
        //   Microsoft.Reporting.WinForms.ReportParameter
        // Parameters the cloud service is expecting are of the type
        //   Microsoft.Reporting.WebForms.ReportParameter
        // The parameters accepted by the method are mapped to a list
        //   of parameters of web forms type before being added to
        //   the data transfer object
        List<CloudService.Service.ReportParameter> cloudParameters = new List<CloudService.RTRService.ReportParameter>();

        if (parameters.Count > 0)
        {
          foreach (ReportParameter rp in parameters)
          {
            List<object> cloudValues = new List<object>();

            foreach (string s in rp.Values)
              cloudValues.Add(s);

            cloudParameters.Add(new CloudService.Service.ReportParameter { m_name = rp.Name, m_value = cloudValues, m_visible = rp.Visible });
          }
        }

        CloudService.Service.ReportDTO rdto = new CloudService.Service.ReportDTO();
        rdto.reportParameters = cloudParameters;
        rdto.reportPath = reportPath;
        rdto.reportProcessingMode = CloudService.Service.ProcessingMode.Remote;

        ServiceRequests.ServiceRequests.service = new ServiceRequests.ServiceRequests(MyApp.Authentication);
        MemoryStream stream = service.Report(rdto);
        DataTable reportData = new DataTable { TableName = "Report" };
        BinaryFormatter formatter = new BinaryFormatter();
        reportData = (DataTable)formatter.Deserialize(stream);
        _report.LocalReport.DataSources.Add(new ReportDataSource("Report", reportData));
        _reportPath = reportPath;
        _report.RefreshReport(); 
      }
      // The code making the call to this method is checking for an error
      return false;
    }
    catch (Exception ex)
    {
      // Log Exception
    }
  }

The service request service.Report(ReportDTO) is in a separate file for service requests

public MemoryStream Report(ReportDTO rdto)
{
  ServiceClient service = null;

  try
  {
    service = new ServiceClient();
    service.InnerChannel.OperationTimeout = new TimeSpan(0,5,0);
    service.Open();

    ReportDTORequest request = new ReportDTORequest();
    request.Authentication = _authentication; // global property
    request.Entities = new List<ReportDTO>();
    request.Entities.Add(rdto);

    return service.Report(request).Entities.FirstOrDefault();
  }
  catch (Exception ex)
  {
    throw ex;
  }
  finally
  {
    if (service != null)
    {
      service.Close();
    }
  }
}

The request is received by an operation contract in the cloud project.

[WebInvoke(Method = "POST")]
[OperationContract]
public Response<MemoryStream> Report(Request<ReportDTO> request)
{
  Response<MemoryStream> response = new Response<MemoryStream>();
  response.Status = ResponseStatus.FAILED;

  try
  {
    if ((request != null) && (request.Entities != null))
    {
      if (request.Authentication != null)
      {
        // I know this part is unusual but it is working around a complication between an old custom response object and a new custom response object to replace the old one, which is still being used elsewhere
        KeyValuePair<ResponseStatus, string> kvp = request.Authentication.Authenticate(_globalAuthenticationToken);
        response.Status = kvp.Key;
        response.Messages.Add(kvp.Value);

        if (response.Status == ResponseStatus.SUCCESS)
        {
          ReportDTO rdto = request.Entities.FirstOrDefault();

          if ((rdto != null) && (!String.IsNullOrEmpty(rdto.reportPath)))
          {
            // Get settings from database and populate in string variables username, password, domain, and uri

            Microsoft.Reporting.WebForms.ReportViewer rv = new Microsoft.Reporting.WebForms.ReportViewer();

            rv.ServerReport.ReportPath = rdto.reportPath;
            rv.ServerReport.ReportServerUrl = new Uri(uri);
            rv.ServerReport.ReportServerCredentials = new CustomReportCredentials(username, password, domain);

            rv.ServerReport.Refresh();

            if ((rdto.reportParameters != null) && (rdto.reportParameters.Count > 0))
            {
              rv.ServerReport.SetParameters(rdto.reportParameters);
            }

            string mimeType;
            string encoding;
            string extension;
            string[] streamIDs;
            Microsoft.Reporting.WebForms.Warning[] warnings;

            byte[] bytes = rv.ServerReport.Render("Excel", null, out mimeType, out encoding, out extension, out streamIDs, out warnings);

            if ((bytes != null) && (bytes.Count() > 0))
            {
              BinaryFormatter formatter = new BinaryFormatter();
              MemoryStream stream = new MemoryStream();
              formatter.Serialize(stream, bytes);
              response.Entites.Add(stream);
              stream.Close();
              response.Status = ResponseStatus.SUCCESS;
            }
            else
            {
              response.Messages.Add("Unable to render server report");
              foreach (Microsoft.Reporting.WebForms.Warning warning in warnings)
              {
                response.Messages.Add(warning.ToString());
              }
              response.Status = ResponseStatus.FAILED;
            }
          }
          else
          {
            response.Messages.Add("Invalid request data");
            response.Status = ResponseStatus.FAILED;
          }
        }
      }
      else
      {
        response.Messages.Add("Unable to authenticate user request");
        response.Status = ResponseStatus.FAILED;
      }
    }
    else
    {
      response.Messages.Add("Invalid request object");
      response.Status = ResponseStatus.FAILED;
    }
  }
  catch (Exception ex)
  {
    // Log Exception
  }
  return response;
}

According to GotReportViewer, a DataTable can be set as a data source for ReportViewer.LocalReport so I have been trying to return this byte array to my client project and get it into a DataTable format to be displayed in the ReportViewer.

While I have not been able to view the actual data coming back from the call to the ReportServer, I know that the reports I have been testing with are not corrupted, as they were being loaded fine in the old version of this project. Additionally, the byte array coming back from the call to ServerReport.Render is a little over 98k bytes in size, so I assume that the report is being correctly returned from the ReportServer to my cloud project. This is why I am fairly certain my problem is with the serialization/deserialization.

The error I am getting comes when control returns to the client project, at the line reportData = (DataTable)formatter.Deserialize(stream);.

The error thrown is Binary stream '0' does not contain a valid BinaryHeader.

I have found many questions on StackOverflow regarding this binary header error, but they have all been either not related to my specific situation, or ended in the assumption that it is a data issue, which I am as close to positive as I am willing to claim to be that this is not.

Most questions I have found regarding making the request to the report server from a wcf application basically said it is difficult, and there were some alternative methods provided, but there were none that I could find which addressed the issue I am having, or which approached the issue the way I have, or which avoided giving the WPF application access to the authentication information.

So far I have tried:

  • Making the call as just rv.ServerReport.Render("Excel");
  • Returning the byte[] to the client project directly instead of as a MemoryStream
  • Probably a few other variations on casting to a data table (it's been a long week and I don't remember all of the things I've tried precisely)

I have been unable to find a manner in which to directly convert the results of rv.ServerReport.Render into a DataTable.

Please let me know if more (or just different) information would be helpful.

  • Okay, if you look at the MemoryStream, can you see if the stream is valid or just empty? – Tom Jun 26 '17 at 15:31
  • There's a lot going on in this question, and whilst I appreciate this is quite complex you might need to work backwards and make a small example project which shows this issue. – Tom Jun 26 '17 at 15:35
  • 1
    @Tom - The MemoryStream object being returned to the WPF client application is not null or empty. – Katelyn Polahar Jun 26 '17 at 15:39

2 Answers2

0

I think the Excel file is generating error data, or garbage data. Will you share the Excel content

0

ServerReport.Render produces a byte[] array representing an excel report. You are then using BinaryFormatter to convert the byte array into a formatted byte array (I think this is where it's getting confusing).

BinaryFormaters are for converting from one type to bytes and back again. That makes sense in this case, however it can only deserialise objects it itself also serialised. As such:

 BinaryFormatter formatter = new BinaryFormatter();
 MemoryStream stream = new MemoryStream();
 formatter.Serialize(stream, bytes);
 response.Entites.Add(stream);
 stream.Close();

Can be written as this, whereby we instead only transfer the bytes to the client:

response.Entities.Add(bytes)

Thus providing you with simply the byte array representing a rendered excel report on the client. The next trick is to work out how the client can understand this format and convert it into usable data.

My current bet is that on the client side you can call:

var report = new LocalReport();
using (var memStream = new MemoryStream(service.Report(rdto)))
     report.LoadReportDefinition(memStream);
}

And hopefully report will be a valid report.

Tom
  • 2,830
  • 1
  • 18
  • 22
  • Ah, returning the byte[] in full probably would resolve both these suggestions so this is probably invalid. – Tom Jun 26 '17 at 15:52
  • I did try the byte array, but I can try that flush anyway. I'm almost to the point where I'd try a blood sacrifice haha. And to answer your question, `response.Entities.Add(stream)` just adds the MemoryStream to the list contained in the custom response object. – Katelyn Polahar Jun 26 '17 at 15:53
  • I added the flush and a using around the memorystream on the cloud side, before passing it back to the client, but now the stream object being received on the client side is throwing a System.ObjectDisposedException. Should the using be on the client side and *not* on the cloud side? (Sorry, I am admittedly very lacking in precise knowledge of how these streams are handled in memory). – Katelyn Polahar Jun 26 '17 at 16:00
  • Interesting, so the BinaryFormatter is correctly parsing the fact it's a MemoryStream. Okay, don't have the Using statement, but do set `stream.Position = 0;` after the Flush. – Tom Jun 26 '17 at 16:02
  • OOOOOHHHH new error! New error is good! Breaking now again at the line `reportData = (DataTable)formatter.Deserialize(stream);` but now the error is `Unable to cast object of type 'System.Byte[]' to type 'System.Data.DataTable'` where previously it was throwing the no binary header error. – Katelyn Polahar Jun 26 '17 at 16:10
  • Okay, so I get the problem now. You're trying to convert a byte array representing a rendered Excel report into a DataTable. Try deserialise into a Report object instead? – Tom Jun 26 '17 at 16:19
  • I can try deserializing into a ReportDataSource object. The ReportViewer.LocalReport is read only. – Katelyn Polahar Jun 26 '17 at 16:23
  • Nope. InvalidCastException. – Katelyn Polahar Jun 26 '17 at 16:25
  • Okay, on the server, try the line `var test = new Report(); test.LoadReportDefinition(stream)` just after you get stream from `byte[] bytes = rv.ServerReport.Re...`. Does that make a valid object? – Tom Jun 26 '17 at 16:28
  • Try LocalReport? – Tom Jun 26 '17 at 16:39
  • Ok sorry. Solution wasn't updating service reference properly. Had to delete and re-add it. LoadReportDefinition will not accept an argument of type byte[] it wants a TextReader... – Katelyn Polahar Jun 26 '17 at 17:00
  • See my updated answer, you want to wrap the bytes in a memory stream for it :) – Tom Jun 26 '17 at 17:07
  • So now it redirects to the view for the report (it gets past the call without throwing an error inside the code) but instead of the report displays `An error occurred during local report processing. The definition of the report '' is invalid. The report definition is not valid. Details: Data at the root level is invalid. Line 1, position 1.` Will investigate. – Katelyn Polahar Jun 26 '17 at 17:10
  • Bit of a shot in the dark, but if you look at the bytes from the Render function, are they actual text? I know it's dull but if you convert them with ASCII or UTF do they represent a XML or HTML file or something? `Render` normally implies they create some form of HTML. – Tom Jun 26 '17 at 17:34
  • I hadn't the foggiest clue where to even begin to try to read the data that is in the byte array. I can give it a shot though. – Katelyn Polahar Jun 26 '17 at 18:05
  • string test = Encoding.ASCII.GetString(bytes) gives me a bunch of question marks and \0's....UTF8 and Unicode were equally gibberishy – Katelyn Polahar Jun 26 '17 at 18:25