Меню

Как вывести в отчет SSRS комментарий, присланный от исполнителя Project Server?

Добрый день, дорогие коллеги читатели, поступил запрос как вывести в отчет SSRS заметки ответственных лиц. Заметки имеют значительную ценность (при правильной постановке задачи управления), потому что без исходной информации проблемы не классифицировать и правильное решение не принять. Их, заметки, конечно, надо выводить в отчеты.

С точки зрения пользователя все вроде бы просто — отчет он и есть отчет, но с технической точки зрения открывается масса нюансов и вопросов. В этой статье я привожу свое решение, надеюсь, что оно пригодится моим коллегам (читателям).

Заметки, хранятся в поле с названием TASK_RTF_NOTES. Поле заполняется пользователем при передаче отчетов по задачам, за которые он отвечает. Тут вроде бы нет особой необходимости в средствах разметки, более того, они, эти средства, от пользователя скрыты, т.к. заметки вводятся не только из web, но и с Project Professional, в БД данная информация храниться в формате RTF. Все бы ничего, но надо подготавливать отчеты, в том числе не по одному проекту, а по куче разом. Хорошим инструментом для подготовки отчетов является SSRS.

Как же нам решить данную задачу для это потребуется:

  • Report Builder
  • Management Studio (для удобства)
  • Visual Studio
  • Знание C# и T-SQL

Для начала напишем не сложный запрос выбирающее задачи по проекту с заметками на T-SQL:

Для Project Server 2013 запрос выглядит следующим образом:
select
t.PROJ_UID as [ProjectUID]
,p.PROJ_Name as [ProjectName]
,t.TASK_UID as [TaskUID]
,t.TASK_NAME  as [TaskName]
,t.TASK_RTF_NOTES  as [TaskNotes]
from [pub].MSP_TASKS AS t (nolock)
inner join [pub].MSP_PROJECTS as p (nolock) on  p.PROJ_UID = t.PROJ_UID
where
t.PROJ_UID = @ProjUId

т.к. схема в Project Server 2016 изменена, то модифицируем запрос следующим образом:
select
t.PROJ_UID as [ProjectUID]
,p.PROJ_Name as [ProjectName]
,t.TASK_UID as [TaskUID]
,t.TASK_NAME  as [TaskName]
,t.TASK_RTF_NOTES  as [TaskNotes]
from pjpub.MSP_TASKS AS t (nolock)
inner join pjpub.MSP_PROJECTS as p (nolock) on  p.PROJ_UID = t.PROJ_UID
where
t.PROJ_UID = @ProjUId

Отлично выборка возвращает, на необходимую информацию.
Теперь сделаем шаблон отчета SSRS, для этого воспользуемся построителем отчетов Report Builder, настройку источников данный я опущу, мы это рассматривали в статьях ранее.
За пару минут сверстаем простенький отчет:

Как видим вместо комментария нам отображается ошибка (Код ошибки).

Далее нам потребуется Visual Studio (я буду использовать VS 2012)

Создадим новый проект (Class Library)

Сборка должна быть подписана.

Далее не много кода:
public class RtfConverter
{

internal static string Left(string str, int length)
{
return str.Substring(0, Math.Min(length, str.Length));
}

internal static string DecodeRtfEncoding(string s, int codePage)
{
string pat = @»\\'(\w\w)»;

System.Text.RegularExpressions.Match m = System.Text.RegularExpressions.Regex.Match(s, pat, System.Text.RegularExpressions.RegexOptions.IgnoreCase);

while (m.Success)
{
System.Text.RegularExpressions.Capture c = m.Groups[0].Captures[0];

System.Console.WriteLine(c);

m = m.NextMatch();
}

string resultString = System.Text.RegularExpressions.Regex.Replace(s,pat,delegate(System.Text.RegularExpressions.Match match)
{
string code = match.Groups[1].Captures[0].Value;
Encoding wind1251 = Encoding.GetEncoding(codePage);
Encoding utf8 = Encoding.UTF8;
byte[] wind1251Bytes = new byte[] { (byte)UInt16.Parse(code.ToString(), System.Globalization.NumberStyles.HexNumber) };
byte[] utf8Bytes = Encoding.Convert(wind1251, utf8, wind1251Bytes);
string utf8String = Encoding.UTF8.GetString(utf8Bytes);

return utf8String;
},
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
return resultString;
}

internal static string ConvertRtfToTextRegex(string input)
{
return Regex.Replace(Regex.Replace(input, @»\{\\\*\\fname ([^;]+);\}\1 CYR;», string.Empty), @»\{\*?\\[^{}]+}|[{}]|\\\n?[A-Za-z]+\n?(?:-?\d+)?[? ]?», string.Empty);
}

public string byteArrayToString(Byte[] b)
{
StringBuilder s = new StringBuilder();

string mystr = string.Empty;

if (b != null)
{
try
{
mystr = Encoding.UTF8.GetString(b);
return DecodeRtfEncoding(ConvertRtfToTextRegex(mystr), 1251).Trim(new char[] { ‘\n’, ‘\r’, ‘ ‘, ‘\0’, (char)65533 });
}
catch (Exception ex)
{
return ex.ToString();
}
}

return mystr;
}
}

Отлично наша библиотека для конвертации готова.
На сервере где установлен SSRS закидываем наши сборки по адресу:
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\WebServices\Reporting\bin

Возвращаемся в построитель отчетов добавляем references на нашу сборку и класс, как показано скриншоте ниже:

Для поля TaskNotes пишем выражение конвертации:

В приведенном примере я конвертирую в строку, если необходимо сохранить разметку, то можно воспользоваться
Проверяем наш отчет SSRS, выглядит замечательно.

Усложним задачу сохраним разметку в поле комментарий для этого я  воспользуюсь готовой библиотекой https://www.codeproject.com/Articles/27431/Writing-Your-Own-RTF-Converter, для написания статьи большое спасибо автору. Единственное что я сделал это включил все в свою библиотеку и произвел некий для упрощения.

В результате вот что у меня получилось:

Добавим в нашу сборку еще немного кода:
public string ToHtml(byte[] byteArray)
{
return RtfToHtml(byteArray);
}

public static string RtfToHtml(byte[] byteArray)
{
if (byteArray == null || byteArray.Length == 0)
{
return string.Empty;
}

try
{
IRtfGroup rtfStructure = null;
try
{
rtfStructure = ParseRtf(byteArray);
}
catch
{
return string.Empty;
}

RtfInterpreterSettings interpreterSettings = new RtfInterpreterSettings();
IRtfDocument rtfDocument = RtfInterpreterTool.BuildDoc(rtfStructure, interpreterSettings/*, imageConverter*/);

RtfHtmlConvertSettings htmlConvertSettings = new RtfHtmlConvertSettings();
htmlConvertSettings.ConvertScope = RtfHtmlConvertScope.Content;
RtfHtmlConverter htmlConverter = new RtfHtmlConverter(rtfDocument);
return (htmlConverter.Convert() ?? string.Empty).Replace(«?», string.Empty);
}

catch (Exception ex)
{
return ex.Message + ex.StackTrace;
}

}

 

private static IRtfGroup ParseRtf(byte[] rtfBytes)
{
IRtfGroup rtfStructure;
using (MemoryStream stream = new MemoryStream(rtfBytes))
{
RtfParserListenerStructureBuilder structureBuilder = new RtfParserListenerStructureBuilder();
RtfParser parser = new RtfParser(structureBuilder);
parser.IgnoreContentAfterRootGroup = true; // support WordPad documents
parser.Parse(new RtfSource(stream));
rtfStructure = structureBuilder.StructureRoot;
}

 

return rtfStructure;

}

Проверим что у нас получилось, предварительно в отчете у Placeholder установим свойство HTML.

Не забываем исправить Expression на =Trim(code.rc.ToHtml(Fields!TaskNotes.Value))

Результат, то что нужно.  Отчет работает как в построители, так и вебе.

Сравним результат с заметкой в Project

Все получилось, спасибо за внимание успехов в ваших проектах.

Готовая сборка для конвертации тут RtfConverter. пароль: 123 🙂