Tuesday, April 20, 2010

Editing Rtf file

Sometimes there is need to edit an .rtf file. To my knowledge, exept the PIA Interop word, there is no free and effective tools to edit such a file. The word Interop problem is performance and also their integration in web applications which often causes problems.


I discovered by accident that the WPF RichTextBox control (exept the fact that it allows to read and write a .RTF document) allows you to edit an .RTF document programmatically. The purpose of this article is to see the possibilities offered by this control.


For this, I created a WPF application to read an .RTF document, insert a row in a table, edit and add text in a paragraph. The interface looks like this:



First, open the .RTF document:


' Open the rtf document
Public Function OpenRtf() As Boolean
' Check the file path
If Not File.Exists(Me._rtfFilePath) Then Throw New ArgumentException("The rtf file does not exist")
' With two TextPointer positions as the beginning and end positions for the new Range
Dim range As New TextRange(Me._rtb.Document.ContentStart, Me._rtb.Document.ContentEnd)
' Exposes a stream around the .RTF File for opening the file
Using fStream As New FileStream(Me._rtfFilePath, FileMode.OpenOrCreate)
' Load the current selection (In this case all the .Rtf file content)
range.Load(fStream, DataFormats.Rtf)
End Using
Return True
End Function

The function to insert a row in a table in the document:


' Insert a row at a specified table index and rows index
Public Function InsertRow(ByVal tableIndex As Integer, ByVal rowIndex As Integer) As Boolean
Dim i As Integer
Dim table1 As Table = Nothing
For j = 0 To Me._rtb.Document.Blocks.Count - 1
' Is the current object type a Tabel ?
If Not TypeOf (Me._rtb.Document.Blocks(j)) Is Table Then Continue For
i += 1
' Is the current table a second one ?
If i < tableIndex Then Continue For
' Get the table at index = tableIndex
table1 = Me._rtb.Document.Blocks(j)
Exit For
Next
' Is there no table in the Rtf file ?
If table1 Is Nothing Then Return False
' Add a new row after a specified row index
table1.RowGroups(0).Rows.Insert(rowIndex, New TableRow())
Dim currentRow As TableRow = table1.RowGroups(0).Rows(rowIndex)
' Global formatting for the row.
currentRow.FontSize = 12
currentRow.FontWeight = FontWeights.Normal
' Add cells with content to the new row.
currentRow.Cells.Add(New TableCell(New Paragraph(New Run("Paris"))))
currentRow.Cells.Add(New TableCell(New Paragraph(New Run("France"))))
currentRow.Cells.Add(New TableCell(New Paragraph(New Run("$50,0"))))
currentRow.Cells.Add(New TableCell(New Paragraph(New Run("$50,0"))))
currentRow.Cells.Add(New TableCell(New Paragraph(New Run("$5,0"))))
' Bold the cells content.
For i = 0 To currentRow.Cells.Count - 1
currentRow.Cells(i).FontWeight = FontWeights.Bold
currentRow.Cells(i).SetValue(TableCell.BorderThicknessProperty, New Thickness(1D))
currentRow.Cells(i).SetValue(TableCell.BorderBrushProperty, Brushes.Black)
Next
Return True
End Function

The "System.Windows.Documents.Block" object is useful to take all document objects, so we get a BlockCollection. Table, Paragraph, List and others inherits from Block Object


Then, to change a paragraph and edit it we do the following:


' Edit a specified paragraph
Public Function EditParagraph(ByVal paragraphIndex As Integer) As Boolean
Dim i As Integer
Dim pgh As Paragraph = Nothing
For j = 0 To Me._rtb.Document.Blocks.Count - 1
' Is the current object type a Tabel ?
If Not TypeOf (Me._rtb.Document.Blocks(j)) Is Paragraph Then Continue For
i += 1
' Is the current paragraph a second one ?
If i < 7 Then Continue For
' Get the third paragraph
pgh = Me._rtb.Document.Blocks(j)
Exit For
Next
' Is there no paragraph in the Rtf file ?
If pgh Is Nothing Then Return False
' Create two TextPointers that will specify the text range the Span will cover
Dim myTextPointer1 As TextPointer = pgh.ContentStart.GetPositionAtOffset(10)
Dim myTextPointer2 As TextPointer = pgh.ContentEnd.GetPositionAtOffset(-5)
' Create a Span that covers the range between the two TextPointers.
Dim mySpan As Span = New Span(myTextPointer1, myTextPointer2)
mySpan.Background = Brushes.Red
' Add some text
pgh.Inlines.Add(New Run(". Some others informations can be found here: "))
' Add a link
Dim run3 As New Run("MSDN")
Dim hyperl As New Hyperlink(run3)
hyperl.NavigateUri = New Uri("http://msdn.microsoft.com/fr-fr/library/ms754030%28v=VS.100%29.aspx")
pgh.Inlines.Add(hyperl)
Return True
End Function

Finally, we save the document:


' Save the rtf document
Public Function SaveRtf() As Boolean
'initializes a new instance of TextRange
Dim range As New TextRange(Me._rtb.Document.ContentStart, Me._rtb.Document.ContentEnd)
' Exposes a stream around the .RTF File for opening the file
Using fStream As New FileStream(Me._rtfFilePath, FileMode.OpenOrCreate)
' Save the current selection (In this case all the document)
range.Save(fStream, DataFormats.Rtf)
End Using
Return True
End Function

In this article, we reviewed how to edit an .RTF document without using a third-party tool.
The following links are useful for understanding the extent of possibiltés offered by the "System.Windows.Documents" namespace:


MSDN "System.Windows.Documents Namespace"
Kirupa.Chinnathambi Article


zip WpfRTBToRTF.zip - 100 Kio

Sunday, April 11, 2010

Zip/Unzip with PowerShell

Files compression is a common task in software development and some times, some one need to write a script for a repetitive task like log files compression. In this case PowerShell is useful.

The following script show how we can doing a folder compression :


#Select folder for compression
function Select-FolderBrowserdDialog
{
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$objForm = New-Object System.Windows.Forms.FolderBrowserDialog
$objForm.RootFolder = "Desktop"
$Show = $objForm.ShowDialog()
If ($Show -eq "OK")
{
Return $objForm.SelectedPath
}
Else
{
Write-Error "Operation cancelled by user."
}
}

#Call function to open FolderBrowserdDialog
$dir = Select-FolderBrowserdDialog
#Set zip file name
$zipfilename = $dir + ".zip"
#is zip filz exists ?
if(-not (test-path($zipfilename))){
#Create zip file (the header required to convince Windows shell that this is really a zip file)
set-content $zipfilename ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
#Set zip file writable
(dir $zipfilename).IsReadOnly = $false
}else{
#Throw exception if zipe file already exists
Throw "zipe file: " + $dir + ".zip" + " already exists"
}
#Instantiate com object "shell.application"
$shellApplication = new-object -com shell.application
#Set reference between Shell.Application namespace and a zip file
$zipPackage = $shellApplication.NameSpace($zipfilename)
#Get back information about the objects in the folder
$input = Get-ChildItem $dir
#Iterate over files information list
foreach($file in $input)
{
#get count of already zipped files
$zippedCount = $zipPackage.Items().Count
#compress the current file
#"CopyHere" take as parameters:
#file full name
#Some vOptions:
#8: Give the file being operated on a new name in a move, copy, or rename operation if a file with the target name already exists.
#64: for Responding with "Yes to All" for any dialog box that is displayed.
#256: Display a progress dialog box but do not show the file names.
$zipPackage.CopyHere($file.FullName,8 -and 16 -and 256)
#"CopyHere" function is asynchronous, so we have to wait until the current file is not compressed
while($zipPackage.Items().Count -lt ($zippedCount + 1)){
$maxLoops = 60*100 # 1 minutes
#The thread stop if file compression take more than 1 minute
if (--$maxLoops -le 0){
#Throw exception when file compression exceed 1 minute
Throw "timeout exceeded"
}
#Waiting 10 milliseconds for each file compression
Start-Sleep -milliseconds 10
}
}

Two functions are particularly interesting: "Select-FolderBrowserdDialog" and "CopyHere", the first one is for selecting a Folder path and the second is for adding files to a .zip archive (unfortunately this function is asynchronous, the while loop is to prevent the next file to be added at the same time).


On the other hand now we need to unzip the archive, the following script shows how to perform a folder decompression:


#Select zip file for decompression
function Select-OpenFileDialog
{
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$objForm = New-Object System.Windows.Forms.OpenFileDialog
$objForm.InitialDirectory = "E:\"
$objForm.Filter = "Zip file (*.zip)|*.zip|All Files (*.*)|*.*"
$objForm.Title = $Title

$Show = $objForm.ShowDialog()
If ($Show -eq "OK")
{
Return $objForm.FileName
}
Else
{
Write-Error "Operation cancelled by user."
}
}
#Get selected zip file name
$zipfilename = Select-OpenFileDialog
#Get Directory full name
$destination = [System.IO.Path]::GetDirectoryName($zipfilename)
#is zip file exist ?
if(test-path($zipfilename))
{
#Instantiate com object "shell.application"
$shellApplication = new-object -com shell.application
#Set reference between Shell.Application namespace and a zip file
$zipPackage = $shellApplication.NameSpace($zipfilename)
#Set reference between Shell.Application namespace and a directory
$destinationFolder = $shellApplication.NameSpace($destination)
#decompress the zip file
#"CopyHere" take as parameters:
#zip file items
#Some vOptions:
#8: Give the file being operated on a new name in a move, copy, or rename operation if a file with the target name already exists.
#64: For Responding with "Yes to All" for any dialog box that is displayed.
#256: Display a progress dialog box but do not show the file names.
$destinationFolder.CopyHere($zipPackage.Items(),8 -and 16 -and 256)
}

In this article, we have seen how we can zip and unzip files by using PoweShell. I hope this article has inspired you !

The following links were helpful to me:

David Aiken article
Msdn learning resources


zip ZipUnzip.zip - 1.9 Kio

Monday, April 5, 2010

Persisting selections

It is sometimes useful to store the user actions. In this article we will see how to store the ListBox selected items to Session variable. Initially, we add three ListBox that we bind with an XmlDataSource:


...
<div>
<asp:ListBox runat="server" ID="LBEmployee" SelectionMode="Multiple"
DataTextField="Name" DataValueField="ID"/>
<asp:ListBox runat="server" ID="LBService" SelectionMode="Multiple"
DataTextField="Service" DataValueField="Service"/>
<asp:ListBox runat="server" ID="LBSite" SelectionMode="Multiple"
DataTextField="Site" DataValueField="Site"/>
<asp:Button ID="ButtonStoreSelection" runat="server"
Text="Store ListBoxes selection" OnClick="ButtonStoreSelection_Click"/>
<asp:LinkButton ID="LBAnotherPage" runat="server"
Text="Go to another page >>" PostBackUrl="~/Index.aspx"/>
</div>
<asp:XmlDataSource ID="XDSCompany" runat="server"
DataFile="~/App_Data/Company.xml" TransformFile="~/App_Data/Company.xsl"/>
...

Tha page look like this:



In the OnClick Button Event we store user selections. And in the PageLoad Event we bind ListBoxes:


...
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
//Bind ListBoxes
this.bindEmployee();
this.bindSite();
this.bindService();
//Get old selections from Session variable
Helper.SelectedItemsFromSession(this.LBEmployee, this.LBSite, this.LBService);
}
}

/*ButtonStoreSelection Click Event*/
protected void ButtonStoreSelection_Click(object sender, EventArgs e)
{
//Get IEnumerable from ListBoxes selections with Extension method
Helper.SelectedItemsToSession(this.LBEmployee.GetSelectedValues<Guid?>(), this.LBSite.GetSelectedValues<string>(), this.LBService.GetSelectedValues<string>());
}

/*Bind Employee ListBox*/
protected void bindEmployee()
{
this.XDSCompany.XPath = "Company/Employees/Employee";
this.LBEmployee.DataSource = XDSCompany;
this.LBEmployee.DataBind();
}

/*Bind Site ListBox*/
protected void bindService()
{
this.XDSCompany.XPath = "Company/Services/Service";
this.LBService.DataSource = XDSCompany;
this.LBService.DataBind();
}

/*Bind Service ListBox*/
protected void bindSite()
{
this.XDSCompany.XPath = "Company/Sites/Site";
this.LBSite.DataSource = this.XDSCompany;
this.LBSite.DataBind();
}

Two methods are interesting in this listing: "SelectedItemsToSession" And "SelectedItemsFromSession". The first one store selections to Session like this:


public static class Helper
{
/*Session variable key*/
private const string CompanyFiltre = "CompanyFiltre";

/*Set Filter*/
public static void SelectedItemsToSession(IEnumerable<Guid?> lstEmployee, IEnumerable<string> lstSite, IEnumerable<string> lstService)
{
//Remove old selections from Session variable
HttpContext.Current.Session[Helper.CompanyFiltre] = null;
//Create XML Element with selected items
XElement xmlFiltre = new XElement("Fields",
new XElement("Employee",
from emp in lstEmployee
select new XElement("value", emp.Value)),
new XElement("Site",
from site in lstSite
select new XElement("value", site)),
new XElement("Service",
from service in lstService
select new XElement("value", service))
);
HttpContext.Current.Session[CompanyFiltre] = xmlFiltre.ToString();
}
...
}

"xmlFiltre" variable contains something like this:


<Fields> 
<Employee>
<value>1b8f4c24-4cb6-4f27-bc0a-9c84f63ef59f</value>
<value>ca10c2c4-5f61-4504-b831-ac4fc8cfda3a</value>
<value>94c06b32-ac11-4fe0-9d7b-b133625249f7</value>
</Employee>
<Site>
<value>Paris</value>
<value>New york</value>
<value>Cairo</value>
</Site>
<Service>
<value>Purchasing</value>
<value>Information Services</value>
<value>Research and Development</value>
</Service>
</Fields>

The second function "SelectedItemsFromSession" get the selected values stored in Session variable:


/*Get filter*/
public static void SelectedItemsFromSession(ListBox LBEmployee, ListBox LBSite, ListBox LBService)
{
//Exit if Session variable is null
if (HttpContext.Current.Session[CompanyFiltre] == null) return;
//Get string variable and Parse it to XElement
string strFiltre = (string)HttpContext.Current.Session[CompanyFiltre];
XElement xmlFiltre = XElement.Parse(strFiltre);
//XML to IEnumerable, thanks to "Linq to XML"
IEnumerable<string> items = from r in xmlFiltre.Element("Employee").Descendants("value") select r.Value;
//Iterate over Employees items and set their values to ListBoxes
foreach (string valEmployee in items)
LBEmployee.Items.Cast<ListItem>().Where(x =>String.Compare(x.Value,valEmployee,true)==0).SingleOrDefault().Selected = true;
//Sites
items = from r in xmlFiltre.Element("Site").Descendants("value") select r.Value;
foreach (string valSite in items)
LBSite.Items.Cast<ListItem>().Where(x => x.Value == valSite).SingleOrDefault().Selected = true;
//Services
items = from r in xmlFiltre.Elements("Service").Descendants("value") select r.Value;
foreach (string valService in items)
LBService.Items.Cast<ListItem>().Where(x => x.Value == valService).SingleOrDefault().Selected = true;
}

This article describe how we can simply persisting user selections, this can be useful when you want save a grid filter. I hope this helps you out !


zip SelectionToSession.zip - 5.9 Kio

Saturday, April 3, 2010

Sliding error message

Among the frequent tasks in the ASP.NET applications is errors handling and displaying informational labels.


To display message, I use JQuery and particularly the slideDown function:



$(document).ready(function () {
if ($("#lblMessage").text().length == 0) {
return false;
}
if ($("#divMessage").length < 1) {
//If the message div doesn't exist, create it
$("body").append("<div id='divMessage' class='Message'><span id='spanMessage'<</span<<a href='#' class='close-notify'><img src='Styles/images/close.png' class='close' alt='close'/></a></div>");
}
//Else, update the text
$("#spanMessage").html($("#lblMessage").text());
//Fade message in
$("#divMessage").slideDown('fast');
//it's possible to Fade message out in 5 seconds for example
//setTimeout('$("#divMessage").hide("slow")', 5000);
$("#divMessage a.close-notify").click(function () {
$("#divMessage").slideUp("fast");
return false;
});
});

So i put my script inside JScript.js, after this point we need to perform some code in the Server code, so first, i add a Label in the MasterPage like this:

....<head runat="server">

<title></title>

<link href="~/Styles/Site.css" rel="stylesheet" type="text/css" />

<asp:ContentPlaceHolder ID="HeadContent" runat="server">

</asp:ContentPlaceHolder>

<script src="Scripts/jquery-1.3.2.min.js" type="text/javascript"> </script>

<script src="Scripts/JScript.js" type="text/javascript"> </script>

</head>

<body>

<form runat="server">

<asp:label id="lblMessage" ViewStateMode="Disabled" runat="server" />.....

In the child page, i perform some code wich cause an error :



public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
try
{
if (!this.Page.IsPostBack) return;
int z = 0;
double a = 1 / z;
}
catch (DivideByZeroException ex)
{
Label lblMessage = Master.FindControl("lblMessage") as Label;
lblMessage.Text = String.Format("An unexpected error occurred : <br/>{0}", ex.Message);
}

}
}

And finally some style :



div.Message
{
margin:1px;
vertical-align: middle;
width: 97%;
position: absolute;
top: 0px;
border: 1px dashed #FF9900;
display: none;
background-color: #ffffcc;
font-family: "Segoe UI", Tahoma, Arial, Helvetica, sans-serif;
font-size: 11pt;
background-image: url(images/warning.png);
background-repeat: no-repeat;
padding-left:30px;
}

.close-notify {
white-space: nowrap;
float:right;
margin:2px;
}

.close
{
border-style: none;
}

#lblMessage
{
display: none;
}

It's look like this:



This article describe how we can display message errors into MasterPage using JQuery, this code is extensible for other cases. I hope this helps you out !



zip MessageError.zip - 133.6 Kio