Portable distribution of .Net applications with Microsoft Report Viewer and Oracle Instant Client reports

  • Tutorial

Quite often there is a need or desire to refuse to create an installer and distribute the application by copying the file folder to the target computer. If you are interested in how to create a portable distribution kit for .Net applications with Report Viewer reports or how to easily copy the client and driver to access the Oracle database, please, use the cat. I will try to explain everything in detail.

As a practical part, we will consider creating an application that displays reports for the Supermag trading system (which, in fact, uses the Oracle database).

One of the advantages of portable distribution of the Oracle client is that its standard installation is not a pleasant experience. And if you also install Report Viewer, the installation process on several machines runs the risk of becoming a tedious task. Of course, you can use ClickOnce as an alternative, but when distributing using ClickOnce, it is also quite possible to copy the library folder as is done in this example.

Essential libraries for a desktop application with portable access to Oracle

You must download Oracle Instant Client. If you use a localized application, it’s better to take the basic package, although it takes up about 100Mb in size, but, unlike the “light” 30 megabyte, it guarantees you work with the Cyrillic alphabet.

It unpacks the archive and takes 2 files from it:

oci.dll (abbreviation for Oracle Call Interface);
orannzsbb11.dll or orannzsbb12.dll (if you will use version 12).

A third file is also needed:

If you took the basic version, then it will be oraociei11.dll or oraociei12.dll (again for version 12).
If you took the light version - oraociicus11.dll ororaociicus12.dll (I will not mention that the second file for version 12 is clear to everyone).

And you also need the Oracle Data Provider - ODP.NET (it’s better to take the XCopy version, it’s smaller), unpack and find 2 files:

Oracle.DataAccess.dll ;
OraOps11w.dll or OraOps12w.dll (this file is required by Oracle.DataAccess.dll to work with Oracle Instant Client files; it is the same for .Net 2.0 and .Net 4.0).

If you want to use version 12, you can download both Oracle Instant Client and ODP in one ODAC (Oracle Data Access Components) file from the link: www.oracle.com/technetwork/topics/dotnet/downloads/index.html

Essential Libraries for the Portable Report Viewer Application

As a portable distribution Report Viewer we take version 2010.

I did not take version 2013 as a version for portable distribution. After I found that Microsoft.ReportViewer.ProcessingobjectModel.dll was missing from it, I had a “template break” and decided that 2010 reports would be fine. If you know how to create a portable distribution of the 2013 version, I’m waiting for your comments. One could even announce a competition, but here's the bad luck - there is no prize.

Download Microsoft Report Viewer Redistributable 2010 and unzip the exe file as an archive.
Among the unpacked files we find reportviewer_redist2010core.cab .
We continue the “nesting doll” and unpack this file in turn.
We find and rename the files as follows:

rename it Microsoft.ReportViewer.Common.dll

rename it Microsoft.ReportViewer.ProcessingobjectModel.dll

rename it Microsoft.ReportViewer.WebForms.dll

rename to Microsoft. ReportViewer.WinForms.dll

If Oracle Client and Report Viewer are installed on the developer's computer (and it must be installed), you can even set links to these files, or simply copy them to folders with exe (to the Debug and Release folders of the project). But Oracle.DataAccess.dll must be placed in the project folder and set the “Copy locally” property to one of the two copy options.

So that the fear of portable distribution is not so great, I will briefly describe the library search algorithm for a desktop .Net application.

Before starting a dll search, the system checks if the dll is already loaded into memory, and whether the dll is already in the list of already known dlls (try looking at the list in the registry atHKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Session Manager \ KnownDLLs ).

The dll search algorithm depends on some factors (for example, whether the SafeDllSearchMode mode is set), but approximately as follows (in the SafeDllSearchMode example it is turned off):

1. The directory from which the application is launched;
2. Current directory;
3. The system directory. You can use the GetSystemDirectory function to get the path to this directory;
4. 16-bit system directory;
5. Windows directory. You can use the GetWindowsDirectory function;
6. Folders that are specified in the PATH system variable.

What is the PATH system variable and where is it located, visually see the following screenshot:

Now let's move on to the practical part.

In a WPF application (and for me XAML is preferable to legacy Forms as a layout editor), the Report Viewer component is added using the WindowsFormsHost control.

So that the example does not look useless, we get in it data from the history of the document.
To store the data required by the report, create a class:

   public class ReportDataSM
        public DateTime EventTime { get; set; }
        public string Doc { get; set; }
        public string UserName { get; set; }
        public string NewState { get; set; }

Now we can create a template file for the appearance of our report (For the visual editor on the developer's computer, when installing Visual Studio, the Microsoft SQL Server Data Tools item must be selected).

Add a report file to the project (Reporting). Set the value of the “Copy locally” property to “Copy if newer”. Then we open the just created report file and go to the tab “Data Sources”. Click “Add a new data source”, select the “Object” type, open our project in the list of objects and find our ReportDataSM class in it, which we select:

Next, to display the rows with data, add a table to the report. When adding the properties of the data set to the query, select our data source and give it some name (in the example DataSet1):

This data source will be specified in the table tab in the DataSetName property. The rdlc file can be edited not only with the visual editor, but also with the help of the XML editor (in this way it is convenient to delete old unnecessary data).

We set the column values ​​from the data of our array (clicking on the lines is simple and intuitive, I will not explain in detail).

Consider the code

Since freezing an interface is a very bad practice, we start the data sampling in a separate stream. For data storage we use a competitive universal (generic) collection of the ReportDataSM class. Add to the variable declarations (at the beginning of the class):

ConcurrentBag list4R;

After we initialize it somewhere in the code:

list4R = new ConcurrentBag();

Since we decided to receive data without blocking the interface, we need a delegate to call the method asynchronously:

delegate void MyGetDataDelegate(string s);

In this example, the delegate takes one string value as a parameter.
The parameter will be the identifier of the document for which it is necessary to obtain data on the history of changes.
The method that is the delegate implementation is a method called getDataMethod.
I bring a simplified code for implementing the method of obtaining data (without processing possible errors):

 void getDataMethod(string docid){
            DataSet dataset = new DataSet();
            string oradb = "Data Source=" + "DATABASENAME" + ";User Id=" + "typeusernamehere" + ";Password=" + "password123" + ";";
// для того, чтобы не указывать полностью строку подключения к базе, необходимо чтобы в папке клиента Oracle 
// находился файл TNSNAMES.ORA с данными, необходимыми для подключения к базе.
            using (OracleConnection conn = new OracleConnection(oradb))
               if (conn.State != ConnectionState.Open) conn.Open();
string sqltext = "select SMDocLog.EVENTTIME, SMDocLog.ID, SMDocLog.USERNAME, NVL(SSDocStates.DOCSTATENAME,'Отправка почтой') as NEWSTATE";
 sqltext = sqltext + " from SMDocLog LEFT JOIN SSDocStates";
 sqltext = sqltext + " ON SMDocLog.DOCTYPE=SSDocStates.DOCTYPE and SMDocLog.NEWSTATE=SSDocStates.DOCSTATE";
 sqltext = sqltext + " WHERE ID='" + docid + "'";
 sqltext = sqltext + " ORDER BY EVENTTIME DESC";
              using (OracleCommand cmd = new OracleCommand())
                    cmd.Connection = conn;
                    cmd.CommandText = sqltext;
                    cmd.CommandType = CommandType.Text;
                        using (OracleDataAdapter adapterO = new OracleDataAdapter(cmd))
                            using (DataSet ds = new DataSet())
// извлекаем данные в датасет, а затем считываем их в нашу коллекцию
                    foreach (DataRow dr in ds.Tables[0].Rows)
                        ReportDataSM rd = new ReportDataSM();
                        rd.EventTime = Convert.ToDateTime(dr["EVENTTIME"]);
                        rd.UserName = dr["USERNAME"].ToString();
                        rd.Doc = dr["ID"].ToString();
                        rd.NewState = dr["NEWSTATE"].ToString();
                            } // end using DataSet
                        } // end using OracleDataAdapter
                    } // end using OracleCommand

Call this method asynchronously with:

   MyGetDataDelegate dlgt = new MyGetDataDelegate(this.getDataMethod);
   IAsyncResult ar = dlgt.BeginInvoke(txtDocN.Text, new AsyncCallback(CompletedCallback), null);

Pay attention to the CompletedCallback method, which is passed as a parameter.
Its implementation is necessary in order to launch any other method after the completion of our called method. In our case, after extracting the data, we need to bind this data to the report and update the interface.

   void UpdateUserInterface()
             ReportDataSource reportDataSource= new ReportDataSource("DataSet1", list4R);
// указываем название источника данных отчета и нашу коллекцию в качестве данных 
             _reportViewer.LocalReport.ReportPath = "ReportSM.rdlc";

But we have a catch that we can update the interface only from the application flow.
To do this, we will call the application interface update method from the dispatcher thread. That is, the contents of our CompletedCallback method will be:

void CompletedCallback(IAsyncResult result)
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new NoArgDelegate(UpdateUserInterface));

Again, to call the method asynchronously, we needed a delegate, which this time does not accept any parameters. Add it to the beginning of our code:

private delegate void NoArgDelegate();

You should get something like this application:

Update: link to the GitHub project (using Oracle.ManagedDataAccess installed from the NuGet package manager)

Also popular now: