Using WinForm Designer for Silverlight Viewer

SharpShooter Reports.Silverlight doesn’t provide standard ability for reports editing by end users. At the moment, we can’t offer our users Silverlight designer though it is in demand.

Speaking about our component, we often use the word “flexible”, and this not just marchitecture. Our product allows the creation of software systems of any complexity and can be flexibly adapted to any task.

I will explain how to create a designer in this article. This designer will allow your users to edit their reports and save changes on the server.

  1. Creating Service (WCF)
    I won’t describe how to create an application and add our component to it. You can read about it in Getting Started or use the pre-designed examples which are delivered with the product. Let’s focus on the expansion of your application functionality.
      One of the limitations of the designer is that the reports should be located on the server as files. It is necessary to select FileReportSlot when adding report to the Report Manager.
    In order to expand the service abilities, we should add the interface. Let’s name it IExtendedReportService. This interface should be a descendant from the PerpetuumSoft.Reporting.Silverlight.Server.IReportService to make Report Viewer (from Silverlight application) connect to ReportService (WCF).
    Add the following methods to the IExtendedReportService interface:
    • GetReportList(bool onlyFileReportSlot) – gets the list of reports from the service. onlyFileReportSlot defines the reports type when loading them, if the value is true, reports of the FileReportType only will be loaded;
    • GetReport(string reportName) – gets a report where reportName is the name of the report which should be got;
    • GetDataSource() – gets data sources which were added to the ReportManager. This should be done in order that end users are able to see the data source structure and drag the elements from the data source tree as they like;
    • SaveStreamReport(byte[] report, string reportName) – saves on the server report changed in the designer. Report is the report serialized into an array of bytes; reportName is the name of the report on the server.
    The code for the IExtendedReportService interface will look as follows:
      public interface IExtendedReportService : IReportService
      {
        /// <summary>
        /// return report list from server
        /// </summary>
        /// <returns>report list</returns>
        [OperationContract]
        ReportItem[] GetReportList(bool onlyFileReportSlot);
    
        /// <summary>
        /// return report by name from fileslot
        /// </summary>
        /// <param name="reportName">report name</param>
        /// <returns>report file</returns>
        [OperationContract]
        Stream GetReport(string reportName);
    
        /// <summary>
        /// return datasource from reportmanager
        /// after serialized into string
        /// </summary>
        /// <returns>datasource</returns>
        [OperationContract]
        string[] GetDataSource();
    
        /// <summary>
        /// save report on server
        /// </summary>
        /// <param name="report">report</param>
        /// <returns>save result</returns>
        [OperationContract]
        bool SaveStreamReport(byte[] report, string reportName);
      }
    
    The ReportItem class contains two fields:
    Name – is a name of a report;
    DisplayName – is displayed name of a report.
    This class is used to pass data about reports.
    The ReportItem class code:
    [DataContract]
    public class ReportItem
    {
    [DataMember]
    public string Name { get; set; }
    
    [DataMember]
    public string DisplayName { get; set; }
    }

    The IExtendedReportService interface is implemented for the ReportService class:
    public class ReportService : ReportServiceBase, IExtendedReportService
    Let’s examine what each interface method performs.

    The GetReportList method gets the list of reports.
    The object of the List collection is created. The reports are added to this object. The reports, created in ReportManager, are taken one by one in an iteration. If the report matches the set conditions, then the object of the ReportItem type is created. The object fields are initialized and the object is added to the result collection. When all the reports are taken one by one, the collection is converted into array and is got.

    The GetReport method gets the report as a stream.
    It is verified if the report with the indicated name is on the server. If there is no such report, then the method gets null. If there is such a report on the server, then the report is read from the disk into byte array, which is recorded into the gotten stream.

    The GetDataSource method gets the information about data sources added to the ReportManager.
    It is verified if ReportManager contains a data source. If there are no data sources then the empty array is gotten. If there are data sources then the ParseDataSources method is invoked. This method handles the Data Sources objects to get their structures. The data source structure is represented as classes that are passed to the client’s application for the further handling.

    The SaveStreamReport method saves the report, passed by the client, on the server. If saving report fails then false is returned. If report saving is successful then true is returned.

    Let’s configure the service in order to use the added methods. In order to do this, change the <endpoint address="" section in the web.config file. Change the PerpetuumSoft.Reporting.Silverlight.Server.IReportService to SilverlightApplication.Web.IExtendedReportService. The <endpoint address="" section should look as follows:

    <endpoint address="" binding="basicHttpBinding" 
    bindingConfiguration="basicHttpBindingConf"
    contract=" SilverlightDemo_SL4.Web.IExtendedReportService"> <identity> <dns value="localhost" /> </identity> </endpoint>
  2. Adding service to the Silverlight application

    In order to use methods of the ReportService from the Silverlight application, we should add service reference to the service in the Silverlight application. We can do it if we follow these steps:

    1. Open the context menu to add Service Reference
    2. Press the “Discover” button to find the launched services in the opened window. It is necessary to select the service to which we want to connect. Indicate the Namespace name in which the service classes will be created.
  3. Adding the ability to get the list of reports from the service in Silverlight application

    At first, let’s change the application design. Divide the page into two parts. The left part will contain the list of reports and the button for loading (invocation of the rendering function) of the report. The right part will contain ReportViewer. In order to do this, I add the code which divides the Grid element into 2 columns. The code should be added into the Grid element with the LayoutRoot name. Set the width of the first column equal to 200:

    <Grid.ColumnDefinitions>
       <ColumnDefinition Width="200"/>
       <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    Add the ListBox element to display report list onto the page. Redefine the template for the elements output in ListBox in order to output DisplayName of the got report. Add the Button element with the “Render” name to load the selected report. Markup will look as follows:

         <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
     
            <Button Click="Button_Click" Content="Render" 
    Grid.Column="1" Margin="3"/>
     
            <TextBlock Text="Report list" Margin="5 3" Grid.Row="0" 
    FontSize="14"/>
            <ListBox Grid.Column="0" Margin="3" Grid.Row="1" 
    x:Name="_reportList" ItemsSource="{Binding}" Grid.ColumnSpan="2" 
    SelectionMode="Single">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding DisplayName}" 
    Margin="3" Grid.Column="0"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
     
        </Grid>

    In order to add the handler for the “Render” button, it is necessary to invoke the context menu by right click on the «Button_Click». Select the «Navigate to Event Handler» item in the context menu.

    We are navigated to the handler method. I will show the code for this handler later. Add two fields (the MainPage.xaml.cs file) to the class:

    ObservableCollection<ReportItem> _reportCollection = 
    new ObservableCollection<ReportItem>(); 
    ServiceReference1.ExtendedReportServiceClient _client = 
    new ServiceReference1.ExtendedReportServiceClient();
    

    The _reportCollection object stores the report collection which was got from the service. The _client object is the client for connecting to the ReportService we created earlier.
    Add the handler for loading page event (Loaded) to make it possible to perform initialization of the objects after the page is loaded:

        public MainPage()
        {
          InitializeComponent();
          Loaded += new RoutedEventHandler(MainPage_Loaded);
          
        }
        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
          _reportList.DataContext = _reportCollection;
          reportViewer.ServiceUrl = System.Windows.Browser.HtmlPage.
    Plugin.GetProperty("initParams").ToString();
    
          _client.GetReportListCompleted += 
    new EventHandler<ServiceReference1.
    GetReportListCompletedEventArgs>(_client_GetReportListCompleted);
    
          _client.GetReportListAsync(true);
        }
    

    Add the _client_GetReportListCompleted method to handle the response from the service:

        void _client_GetReportListCompleted(object sender, 
    ServiceReference1.GetReportListCompletedEventArgs e)
        {
          if (e.Error == null)
          {
            if (e.Result != null)
            {
              for (int i = 0; i < e.Result.Length; i++)
              {
                _reportCollection.Add(e.Result[i]);
              }
            }
          }
          else
          {
            MessageBox.Show(e.Error.Message);
          }
        }
    

    If there are errors in the response from the service, then the message with the error text appears. You can write any way of bug reporting to inform your client about the error. If there are no errors in the response from the server, then we need to handle the result of the response from the service. If the response contains the data about the report, these data will be added to the _reportCollection collection.
    Let’s write the handler for the “Render” button. We will check if the report is output in the list of reports or not when clicking the button. If the report is output then we get the selected object. Set the report name that should be loaded, and invoke the RenderDocument() method:

        private void Button_Click(object sender, RoutedEventArgs e)
        {
          if (_reportList.SelectedItem != null)
          {
            ReportItem reportItem = _reportList.SelectedItem 
    as ReportItem;
    
            if (!string.IsNullOrEmpty(reportItem.Name))
            {
              reportViewer.ReportName = reportItem.Name;
              reportViewer.RenderDocument();
            }
          }
        }
    
  4. Create designer application.

    Create the “Windows Forms Application”. Name it “WindowsFormsDesigner” (further designer). Add the service to the designer. To add the service we need to perform actions analogous to the actions for service addition to the Silverlight application. The next step, after we added the service, is the creation of the interface for the designer. It is necessary to add the following elements to the form: ListBox, 2 Buttons objects, StatusBar. The ListBox object is used to output of the report list got from the service. One of the buttons is used to invoke the designer; the second button is used to save changes on the server. StatusBar is used to output the information about the program work.
    The application design can look as follows:

    It is also necessary to add the ReportManager component to makeit possible to use the DataSource structure when editing a template. It will be more convenient for a user to change the template if he is be able to create objects that are bound to the data by simple drag-and-drop from the DataSource tree.
    After the designer is created, we need to write the code for application work. Add the handlers for the buttons (Click), form load event (Load) and change of the selected record in ListBox (SelectedIndexChanged).
    In order to save the reports passed from the server, we should add the object of the ObservableCollection type to the Form1 class(the Form1.cs file). Create the object of the ServiceReference1.ExtendedReportServiceClient type for work with the service.
    Add the following code to the form load handler.
    Addition of the handlers for handling the response from the service:

    _client.GetReportListCompleted += new 
    EventHandler<ServiceReference1.
    GetReportListCompletedEventArgs>
    (_client_GetReportListCompleted);
    
    _client.GetReportCompleted += new 
    EventHandler<GetReportCompletedEventArgs>
    (_client_GetReportCompleted);
    
    _client.SaveStreamReportCompleted += new 
    EventHandler<SaveStreamReportCompletedEventArgs>
    (_client_SaveStreamReportCompleted);
    
    _client.GetDataSourceCompleted += new 
    EventHandler<GetDataSourceCompletedEventArgs>
    (_client_GetDataSourceCompleted);

    Getting DataSource from the service:

    _client.GetDataSourceAsync();

    Message output:

    WriteStatus("Loading datasources ...");

    The code of the form load handler looks as follows:

    private void Form1_Load(object sender, EventArgs e)
        {
          _client.GetReportListCompleted += new 
    EventHandler<ServiceReference1.
    GetReportListCompletedEventArgs>
    (_client_GetReportListCompleted);
    
          _client.GetReportCompleted += new 
    EventHandler<GetReportCompletedEventArgs>
    (_client_GetReportCompleted);
    
          _client.SaveStreamReportCompleted += new 
    EventHandler<SaveStreamReportCompletedEventArgs>
    (_client_SaveStreamReportCompleted);
    
          _client.GetDataSourceCompleted += new 
    EventHandler<GetDataSourceCompletedEventArgs>
    (_client_GetDataSourceCompleted);
     
          _client.GetDataSourceAsync();
          WriteStatus("Loading datasources ...");
        }

    In order to load the data about the DataSource it is necessary to invoke the asynchronous method. This method won’t block the application’s work while waiting for response from the server. To do this we will assign the handler to the GetDataSourceCompleted у event in the _client object.
    Let’s examine the code of this method. Before response from the server is handled, we should make sure that the if (e.Error == null) code doesn’t contain any errors.
    If there are no errors then handling of the response from the server starts.
    The response from the server includes a collection of classes, to be more exact, a collection of the classes’ codes, which are used for replication of the structure of the data sources, which are available on the server. In order to create the objects for these classes, we will create the assembly that will be saved on the disk with the .dll extension. To compile the assembly, it is necessary to create the file with this assembly code (the file with the *.cs extension). The following code is used to create and save the file:

    StringBuilder NameSpace = new StringBuilder("namespace 
    DataSourceLib { \n using System; \n");
            NameSpace.Append(CustomPropDescr.
    GetCustomPropertyDescriptionClass());
     
            if (e.Result != null && e.Result.Length > 0)
            {
     
              for (int i = 0; i < e.Result.Length; i++)
              {
                if (e.Result[i].StartsWith("#!_"))
                {
                  NameSpace.Append(string.Format("public 
    partial class {0} : System.ComponentModel.ICustomTypeDescriptor", 
    e.Result[i].Replace("#!_", "").Replace(" ", "_")));
                  NameSpace.Append("{");
                  NameSpace.Append(CustomPropDescr.
    GetICustomTypeDescriptor(e.Result[i].Replace("#!_", "").
    Replace(" ", "_")));
                  NameSpace.Append("}");
                }
                else if (e.Result[i].StartsWith("#_"))
                {
                  dataSourceName.Add(e.Result[i].Replace("#_", ""));
                  NameSpace.Append(string.Format("public 
    partial class {0} : System.ComponentModel.ICustomTypeDescriptor", 
    e.Result[i].Replace("#_", "").Replace(" ", "_")));
                  NameSpace.Append("{");
                  NameSpace.Append(CustomPropDescr.
    GetICustomTypeDescriptor(e.Result[i].Replace("#_", "").
    Replace(" ", "_")));
                  NameSpace.Append("}");
                }
                else
                {
                  NameSpace.Append(e.Result[i]);
                }
              }
            }
            NameSpace.Append("}");
            asmName = Path.Combine(Application.StartupPath, 
    "AssemblySource.cs");
     
            using (StreamWriter sw = new StreamWriter(asmName))
            {
              sw.Write(NameSpace.ToString());
            }

    Now, it is necessary to compile this file into dll. The CompileExecutable method is used. The passed file name, from which the assembly is created, is the argument for this method. We won’t examine this method code since it is very simple, and you can find information on how to create assemblies from files on the MSDN site.

    When the assembly is created, it should be loaded to the application. Since the created assembly is saved to the disk as the file, we will use the static LoadFile method of the Assembly class.

    FileInfo sourceFile = new FileInfo(asmName);
    Assembly assembly = Assembly.LoadFile(String.Format(@"{0}\{1}.dll",
                System.Environment.CurrentDirectory,
                sourceFile.Name.Replace(".", "_")));

    In order to create the object from the loaded assembly, the CreateInstance method of the assembly object is used:

    object obj = assembly.CreateInstance(string.
    Format("DataSourceLib.{0}", dataSourceName[i]));

    where dataSourceName[i] is the name of the class which object we need to create.

    The full method code used to handle response from the server when loading the DataSource:

    if (e.Error == null)
          {
            WriteStatus("Datasources loaded");
            StringBuilder NameSpace = new StringBuilder("namespace 
    DataSourceLib { \n using System; \n");
            NameSpace.Append(CustomPropDescr.
    GetCustomPropertyDescriptionClass());
     
            if (e.Result != null && e.Result.Length > 0)
            {
     
              for (int i = 0; i < e.Result.Length; i++)
              {
                if (e.Result[i].StartsWith("#!_"))
                {
                  NameSpace.Append(string.Format("public 
    partial class {0} : System.ComponentModel.ICustomTypeDescriptor", 
    e.Result[i].Replace("#!_", "").Replace(" ", "_")));
                  NameSpace.Append("{");
                  NameSpace.Append(CustomPropDescr.
    GetICustomTypeDescriptor(e.Result[i].Replace("#!_", "").
    Replace(" ", "_")));
                  NameSpace.Append("}");
                }
                else if (e.Result[i].StartsWith("#_"))
                {
                  dataSourceName.Add(e.Result[i].Replace("#_", ""));
                  NameSpace.Append(string.Format("public 
    partial class {0} : System.ComponentModel.ICustomTypeDescriptor", 
    e.Result[i].Replace("#_", "").Replace(" ", "_")));
                  NameSpace.Append("{");
                  NameSpace.Append(CustomPropDescr.
    GetICustomTypeDescriptor(e.Result[i].Replace("#_", "").
    Replace(" ", "_")));
                  NameSpace.Append("}");
                }
                else
                {
                  NameSpace.Append(e.Result[i]);
                }
              }
            }
            NameSpace.Append("}");
            asmName = Path.Combine(Application.StartupPath, 
    "AssemblySource.cs");
     
            using (StreamWriter sw = new StreamWriter(asmName))
            {
              sw.Write(NameSpace.ToString());
            }
     
            if (CompileExecutable(asmName))
            {
              FileInfo sourceFile = new FileInfo(asmName);
              Assembly assembly = 
    Assembly.LoadFile(String.Format(@"{0}\{1}.dll",
                System.Environment.CurrentDirectory,
                sourceFile.Name.Replace(".", "_")));
    
              for (int i = 0; i < dataSourceName.Count; i++)
              {
                try
                {
                  object obj = assembly.CreateInstance(string.
    Format("DataSourceLib.{0}", dataSourceName[i]));
                  reportManager1.DataSources.
    Add(dataSourceName[i], obj);
                }
                catch (Exception er)
                {
                  Console.WriteLine(er.Message);
                }
              }
            }
            else
            {
              MessageBox.Show("Can't build datasource assembly.");
            }
          }
          else
          {
            WriteStatus(e.Error.Message);
            MessageBox.Show(e.Error.Message, "Error");
          }
          WriteStatus("Loading reports list ...");
          _client.GetReportListAsync(false);

    After the DataSource is handled, the list of reports is requested from the service:

    _client.GetReportListAsync(false);

    The following method is used to handle response from the server when loading reports:

    void _client_GetReportListCompleted(object sender, 
    ServiceReference1.GetReportListCompletedEventArgs e)
        {
          if (e.Error == null)
          {
            if (e.Result != null)
            {
              WriteStatus("Report list was loaded.");
              for (int i = 0; i < e.Result.Length; i++)
              {
                reportsListBox.Items.Add(e.Result[i].DisplayName);
                _reportCollection.Add(e.Result[i]);
              }
            }
            else
            {
              WriteStatus("Report list is empty.");
            }
          }
          else
          {
            WriteStatus(e.Error.Message);
          }
        }

    Checking of errors in the response from the server is executed in the method. If there are no errors, we need check if the response from the server is empty. If it is not equal to null, we will load the data about the reports to ListBox (the reportsListBox object).

    We examined implementation of the basic methods in the designer.
    Let’s examine basic user steps in order to use the designer.

    When a user launches the designer, the query for loading data about the DataSource is sent to the server. When the data about DataSource is handled, the query for the list of reports is sent. When the list of reports is loaded, the user can select the report which design he wants to change and press the “Open designer” button. The designer will be opened. The user can change the report design. In order to save changes on the server, the user needs to press the “Save” button, close the designer, select the report the user wants to save to the server in the list of reports and press the “Save changes on server” button.

Add Feedback