Довольно часто возникает необходимость или желание отказаться от создания инсталлятора и совершать дистрибуцию приложения, копируя папку с файлами на целевой компьютер. Если вам интересно как создать портативный дистрибутив .Net приложения с отчетами Report Viewer или как портативно скопировать клиента и драйвера для доступа к базе Oracle, прошу под кат. Я постараюсь все подробно объяснить.
В качестве практической части будет рассмотрено создание приложения, отображающего отчеты для торговой системы Супермаг (которая, собственно, и использует базу Oracle).
Одним из плюсов портативной дистрибуции клиента Oracle является то, что его стандартная установка — не самое приятное занятие. А если еще устанавливать и Report Viewer, то процесс установки на несколько машин рискует стать утомительным занятием. Конечно же, в качестве альтернативы можно использовать ClickOnce, но при дистрибуции с помощью ClickOnce также вполне можно копировать папку с библиотеками как это делается в данном примере.
Необходимые библиотеки для десктоп приложения со портативным доступом к Oracle
Необходимо скачать Oracle Instant Client. Если вы используете локализованное приложение, то лучше вам взять пакет basic, он хоть и занимает размер около 100Mb, но зато, в отличие от «лайтового» (light) 30 мегабайтного, гарантирует вам работу с кириллицей.
Распаковывает архив и берем из него 2 файлика:
oci.dll (аббревиатура от Oracle Call Interface);
orannzsbb11.dll либо orannzsbb12.dll (если вы будете использовать 12-ю версию).
Необходим еще и третий файлик:
Если вы взяли версию basic, то это будет — oraociei11.dll или oraociei12.dll (опять же для версии 12).
Если вы взяли версию light — oraociicus11.dll или oraociicus12.dll (уже не буду упоминать, что второй файл для версии 12 – это и так всем понятно).
А еще необходим Oracle Data Provider — ODP.NET (лучше взять XCopy версию, — она меньше размером), распаковать и найти 2 файла:
Oracle.DataAccess.dll;
OraOps11w.dll или OraOps12w.dll (этот файл необходим Oracle.DataAccess.dll для работы с файлами Oracle Instant Client он одинаковый и для .Net 2.0 и для .Net 4.0).
Если вы хотите использовать версию 12, можете скачать и Oracle Instant Client и ODP в одном файле ODAC (Oracle Data Access Components) по ссылке: www.oracle.com/technetwork/topics/dotnet/downloads/index.html
Необходимые библиотеки для приложения со портативным Report Viewer
В качестве портативной дистрибуции Report Viewer-а возьмем версию 2010.
Версию 2013 в качестве версии для портативной дистрибуции я не брал. После того, как я обнаружил, что в ней отсутствует Microsoft.ReportViewer.ProcessingobjectModel.dll, у меня произошел «разрыв шаблона» и я решил, что вполне устроят отчеты 2010-го года. Если вы знаете, как создать портативную дистрибуцию версии 2013-го, жду ваших комментариев. Можно было бы даже конкурс объявить, да вот незадача — приза нет.
Качаем Microsoft Report Viewer Redistributable 2010 и распаковаем exe файл как архив.
Среди распакованных файлов находим reportviewer_redist2010core.cab.
Продолжаем «матрешку» и распаковываем в свою очередь и этот файл.
Находим файлы и переименовываем следующим образом:
FL_Microsoft_ReportViewer_Common_dll_117718_117718_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
переименовываем в Microsoft.ReportViewer.Common.dll
FL_Microsoft_ReportViewer_Processingobject_125592_125592_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
переименовываем в Microsoft.ReportViewer.ProcessingobjectModel.dll
FL_Microsoft_ReportViewer_WebForms_dll_117720_117720_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
переименовываем в Microsoft.ReportViewer.WebForms.dll
FL_Microsoft_ReportViewer_WinForms_dll_117722_117722_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
переименовываем в Microsoft.ReportViewer.WinForms.dll
Если на компьютере разработчика установлен Oracle Client и Report Viewer (а он должен быть установлен обязательно), ссылки на эти файлы можно даже на задавать, а просто скопировать их в папки с exe (в папки Debug и Release проекта). А вот Oracle.DataAccess.dll необходимо обязательно поместить в папку проекта и установить свойство «Копировать локально» в какой-нибудь из двух вариантов копирования.
Для того, чтобы страх перед портативной дистрибуцией был не таким большим, опишу вкратце алгоритм поиска библиотеки десктопным .Net приложением.
Перед тем, как запустить поиск dll система проверяет не загружена ли dll уже в память, а также не присутствует ли dll в списке уже известных dll (попробуйте посмотреть список в реестре по адресу HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerKnownDLLs).
Алгоритм поиска dll зависит от некоторых факторов (например, от того, установлен ли режим SafeDllSearchMode), но приблизительно таков (в примере SafeDllSearchMode выключен):
1. Директория, из которой запускается приложение;
2. Текущая директория;
3. Системная директория. Можно использовать функцию GetSystemDirectory для получения пути к этой директории;
4. 16-ти битная системная директория;
5. Директория Windows. Можно использовать функцию GetWindowsDirectory;
6. Папки, которые указаны в системной переменной PATH.
Что такое системная переменная PATH и где она находится, смотрите наглядно на следующем скриншоте:
Теперь перейдем к практической части.
В приложение WPF (а для меня XAML более предпочтителен, чем устаревшие Forms в качестве редактора компоновки) компонент Report Viewer добавляется с помощью элемента управления WindowsFormsHost.
<WindowsFormsHost HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,0,0,0" Name="windowsFormsHost1">
<rv:ReportViewer x:Name="_reportViewer"/>
</WindowsFormsHost>
Чтобы пример не выглядел бесполезным, получим в нем данные из истории документа.
Для хранения данных, необходимых отчету, создадим класс:
public class ReportDataSM
{
public DateTime EventTime { get; set; }
public string Doc { get; set; }
public string UserName { get; set; }
public string NewState { get; set; }
}
Теперь мы может создать файл шаблона внешнего вида нашего отчета (Report Viewer нужной версии для этого, разумеется, должен быть установлен на компьютере разработчика).
Добавим в проект файл отчета (Reporting – Отчет). Установим значение свойства «Копировать локально» в «Копировать, если новее». После чего открываем только что созданный файл отчета и заходим на закладку «Источники данных». Кликнем «Добавить новый источник данных», выбираем типом «Объект», в списке объектов раскрываем наш проект и находим в нем наш класс ReportDataSM, который и выделяем:
Далее для отображения строк с данными добавим в отчет таблицу. При добавлении на запрос свойства набора данных выберем наш источник данных и зададим ему какое-нибудь имя (в примере DataSet1):
Этот источник данных будет указан в табликсе таблицы в свойстве DataSetName (на случай, если вы захотите изменить его позднее).
Устанавливаем столбцам значения из данных нашего массива (кликая на строки – это просто и интуитивно понятно, объяснять подробно не буду).
Рассмотрим код
Так как замораживать интерфейс — это очень плохая практика, выборку данных запустим в отдельном потоке. Для хранения данных используем конкурентную универсальную (generic) коллекцию класса ReportDataSM. Добавим в объявления переменных (в начало класса):
ConcurrentBag<ReportDataSM> list4R;
После инициализируем ее где-нибудь в коде:
list4R = new ConcurrentBag<ReportDataSM>();
Раз данные мы решили получать, не блокируя интерфейс, для вызова метода асинхронно нам понадобится делегат:
delegate void MyGetDataDelegate(string s);
В этом примере делегат принимает одно строковое значение в качестве параметра.
Параметром будет идентификатор документа, для которого необходимо получить данные по истории изменений.
Методом, который является реализацией делегата, пусть будет метод с названием getDataMethod.
Привожу упрощенный код реализации метода получения данных (без обработки возможных ошибок):
void getDataMethod(string docid){
DataSet dataset = new DataSet();
string oradb = "Data Source=" + "DATABASENAME" + ";User Id=" + "supermag" + ";Password=" + "qqq" + ";";
// для того, чтобы не указывать полностью строку подключения к базе, необходимо чтобы в папке клиента 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";
OracleCommand cmd = new OracleCommand();
cmd.Connection = conn;
cmd.CommandText = sqltext;
cmd.CommandType = CommandType.Text;
OracleDataAdapter adapterO = new OracleDataAdapter(cmd);
DataSet ds = new DataSet();
adapterO.Fill(ds);
// извлекаем данные в датасет, а затем считываем их в нашу коллекцию
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();
list4R.Add(rd);
}
}
}
Вызываем этот метод асинхронно с помощью:
MyGetDataDelegate dlgt = new MyGetDataDelegate(this.getDataMethod);
IAsyncResult ar = dlgt.BeginInvoke(txtDocN.Text, new AsyncCallback(CompletedCallback), null);
Обратите внимание на метод CompletedCallback, который передается в качестве параметра.
Его реализация необходима для того, чтобы после завершения нашего вызываемого метода запустить какой-либо другой метод. В нашем случае после извлечения данных нужно привязать эти данные к отчету и обновить интерфейс.
void UpdateUserInterface()
{
_reportViewer.Reset();
ReportDataSource reportDataSource= new ReportDataSource("DataSet1", list4R);
// указываем название источника данных отчета и нашу коллекцию в качестве данных
_reportViewer.LocalReport.ReportPath = "ReportSM.rdlc";
_reportViewer.LocalReport.DataSources.Add(reportDataSource);
_reportViewer.RefreshReport();
}
Но у нас есть загвоздка в том, что интерфейс мы можем обновлять только из потока приложения.
Для этого мы вызовем метод обновления интерфейса приложения из потока диспетчера. То есть содержимым нашего метода CompletedCallback будет:
void CompletedCallback(IAsyncResult result)
{
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new NoArgDelegate(UpdateUserInterface));
}
Опять же, для вызова метода асинхронно нам понадобился делегат, который на этот раз не принимает никаких параметров. Добавим его в начало нашего кода:
private delegate void NoArgDelegate();
Должно получиться примерно такое вот приложение:
Тем, у кого приложение не получилось, может помочь исходник.
Автор: asommer