Friday, 20 February 2009

MsBuild: How to copy/delete all files from subfolders

Copying entire folder content (with subfolders) using standard MsBuild is not as straightforward as I'd imagine this to be. The only solution I've found uses recursion. Following example copies all files from source to dest folder:

C:\source
C:\dest


<AllFolderFiles Include="$(SourceDir)\**\*.*" />

<Copy SourceFiles="@(AllFolderFiles)"
DestinationFiles="@(AllFolderFiles -> $(DestinationDir)\
%(RecursiveDir)%(Filename)%(Extension)')" />

You can also use recursion to delete all files in folder and its subfolders:

C:\source


<FilesToDelete Include="$(ToDelete)\**\*.*" />

<Delete Files="@(FilesToDelete)" />

Anybody knows easier solution?

Thursday, 19 February 2009

Automatic webapp to IIS deployment using MsBuild

Sometimes, when buidling/deploying your projects standard MsBuild tasks are not enough. I've found a very handy SDC tasks library which allows you to do much more from your build script.

The following example demonstrates creation of application pool and website using this AppPool on IIS. Normally this would need to be done manually. Thanks to SDC we can automate the whole process to eliminate possible errors that can occure during manual deployment.

<!-- Use SDC tasks -->
<UsingTask AssemblyFile="Microsoft.Sdc.Tasks.dll" />

<!-- Define website folder -->
<PropertyGroup>
<WebSiteDestDir>
C:\Inetpub\wwwroot_mywebsite
</WebSiteDestDir>
</PropertyGroup>

<!-- Main task -->
<Target Name="Deploy_Webapp">
<CallTarget Targets="CleanIIS" />
<CallTarget Targets="CreateWebSite" />
<CallTarget Targets="CopyContent" />
</Target>

<!-- Clean IIS: remove existing AppPool and Website -->
<Target Name="CleanIIS">
<Web.WebSite.Delete Description="myWebapp" />
<Web.AppPool.Delete AppPoolName="myAppPool" />
</Target>

<!-- Create application pool -->
<Target Name="CreateAppPool" >
<Message Text="Creating my AppPool" />
<Web.AppPool.Create AppPoolName="myAppPool"
WorkerProcesses="1"
IdentityType="NetworkService"
ContinueOnError="false"/>
</Target>

<!-- Create website -->
<Target Name="CreateWebSite" DependsOnTargets="CreateAppPool">
<Message Text="Creating WebSite" />
<Web.WebSite.Create Description="myWebapp"
Path="$(WebSiteDestDir)"
HostName="myWebapp"
AppPoolId="myAppPool"
ContinueOnError="false" />
</Target>

<Target Name="CopyContent" DependsOnTargets="CreateWebSite">
<!-- Copy the app content to WebSiteDestDir here -->
</Target>
Using this library you can also create ActiveDirectory user, virtual directories and a whole lot more!

Wednesday, 11 February 2009

Losing session data on IIS

I had a problem with asp.net app losing session data. The same app was working like a charm on my local machine but after deploying to dev server it was throwing exceptions from time to time caused by missing session variables.

My webapp was using default sessionState settings with mode set to 'InProc' and it was configured to use default application pool. The problem was caused by the application pool using Web Garden with 5 worker processes. When using InProc mode the worker processes DO NOT share session data!

2 possible solutions:
  1. Configure application pool to use 1 worker process: in IIS Manager right click yout application pool > Properties > Performance > Web Garden section
  2. Use different session state mode e.g. state server or sql server (set in web.config)

Tuesday, 10 February 2009

Defining IBatis connection string in web.config

If you are using Ibatis you may want to move the connection string from the SqlMap.config file to the Web.config e.g. to enable its encryption (see previous post).

Solution:
Define your database connection in SqlMap.config as follows:

<database>
<provider name="sqlServer2.0"/>
<dataSource name="DB" connectionString="${dbConnStr}"/>
</database>

Define connection string in Web.config:
<configuration>
...
<connectionStrings>
<add name="dbConnStr" connectionString="some conn str"/>
</connectionStrings>
...
</configuration>

Then, manually configure the builder when initializing the mapper:

// Create builder
DomSqlMapBuilder builder = new DomSqlMapBuilder();

// Get connection string from web.config
ConnectionStringSettings dbConnStr =
ConfigurationManager.ConnectionStrings["dbConnStr"];

// Set dbConnStr property to
// populate in sqlmap.config
NameValueCollection properties = new NameValueCollection();
properties.Add("dbConnStr", dbConnStr.ConnectionString);
builder.Properties = properties;

// Create mapper
ISqlMapper mapper = builder.Configure("SqlMap.config");
IBatis framework is quite handy in use but the documentation is very poor. For more advanced questions you'll need to digg through fora and blogs. I've found this tip here.

Monday, 9 February 2009

Encrypting sections in web.config

Sometimes you want to ensure that your settings (e.g. connection strings) in web.config file are encrypted so nobody except the app iteslf can read/understand them. ASP.Net offers tool called aspnet_regiis which allows that. It can be found in the %WINDOWSDIR%\Microsoft.Net\Framework\version directory.

Solution:
Let's say we have a web app deployed on IIS called 'MyApp'. The app uses connection string defined in web.config:
<configuration>
...
<connectionStrings>
<add name="myConnectionString"
connectionString="some connection string"/>
</connectionStrings>
...
</configuration>

The easiest way to encrypt presented connection string is to invoke following command:
aspnet_regiis
-pe "connectionStrings" -app "/MyApp"

It is also possible to encrypt web.config providing physical path to the application folder rather than app name (e.g. if app is not deployed on IIS):
aspnet_regiis 
-pef "connectionStrings"
"physical path to app root folder"

The encrypted information in the web.config can still be accessed by your app without any explicit decoding. Aspnet_regiis tool can be also used to descrypt information, encrypt different sections etc. You can learn more about it here.

Warning
If the encoding succeeds but tha app cannot read the encrypted section because of following error:
"Failed to decrypt using provider 'RsaProtectedConfigurationProvider'. Error message from the provider: The RSA key container could not be opened."

you have to add following parameter to your encryption command:
aspnet_regiis 
-pe "connectionStrings" -app "/MyApp"
-prov DataProtectionConfigurationProvider

Thursday, 5 February 2009

Adding tooltips to list items in DropDownList

I needed to add tooltips to the items on the standard DropDownList asp control. I'm using custom data source to retrieve the list of items rather than static list defined in html.

Solution:
Add a ondatabound event handler to your DropDownList control:
<asp:DropDownList runat="server"
DataSourceId="MyCustomDatasource"
DataTextField="SomeName"
DataValueField="SomeValueCode"
ondatabound="ApplyOptionTitles"
... />
Then implement handler:
protected void ApplyOptionTitles(object sender, EventArgs e)
{
DropDownList ddl = sender as DropDownList;
if (ddl != null)
{
foreach (ListItem item in ddl.Items)
{
item.Attributes["title"] = item.Text;
}
}
}
Answer found here.

Wednesday, 4 February 2009

Handling exceptions in UpdatePanel & UpdatePanelAnimationExtender

UpdatePanelAnimationExtender allows you to add eye-catching animations to your UpdatePanel indicating that update is in progress. However, it doesn't directly support error handling.

The following code defines UpdatePanelAnimationExtender for UpdatePanel MyUP. The panel will fade out each time the update starts and fade in after successful update.
<cc1:UpdatePanelAnimationExtender
ID="MyUPAE"
BehaviorID="UpdateAnimation"
runat="server"
Enabled="True"
TargetControlID="MyUP">
<Animations >
<OnUpdating >
<Parallel duration="0">
<FadeOut minimumOpacity=".5" />
</Parallel>
</OnUpdating>
<OnUpdated>
<Parallel duration="0">
<FadeIn minimumOpacity=".5" />
</Parallel>
</OnUpdated>
</Animations>
</cc1:UpdatePanelAnimationExtender>
If the code executed during update causes an error the browser may warn you about JS error (caused by actual server side error) but the panel will remain faded out. Browser's warning is usually hard to notice or even not displayed at all.

Solution:
The following JS code allows you handling update errors on client side. It assumes that there is a span element defined on that page for displaying error messages (with ID=ErrorMsg).
<script type="text/javascript">

// Create EndRequest handler for update panel
Sys.WebForms.PageRequestManager.getInstance().
add_endRequest(EndRequestHandler);

function EndRequestHandler(sender, args)
{
// If there is an unhandled error
if (args.get_error() != undefined)
{
// create error message and display it
var errorLbl = $get('<%=ErrorMsg.ClientID%>');
errorLbl.innerHTML = 'An error occurred while completing update.';

// end update panel animation
var upAnimation = $find('UpdateAnimation');
upAnimation._postBackPending = false;
upAnimation.get_OnUpdatingBehavior().quit();
upAnimation.get_OnUpdatedBehavior().play();

args.set_errorHandled(true);
}
}
</script>

Monday, 2 February 2009

"Maximum request length exceeded" error

I have an aspx page using standard FileUpload control. When I tried to upload a file bigger than 4MB the page was not displayed or the following error message appeared: "Maximum request length exceeded". This is caused by maximal allowed size of request accepted by the server. Since the uploaded file is sent in request's body the whole request is even bigger than the file itself.

I've seen some solutions using Application_Error handler defined in Global.asax. This approach is quite straightforward and seems to be a way to go but unfortunately it didn't work for me - the page was still not displayed and the error was not handled.

Solution:
First, set the size of request accepted by the server to maximum (1GB) in web.config. Then, define a custom HttpModule that would check request's length (also in web.config):
<system.web>
<httpRuntime maxRequestLength="102400" />
<httpModules>
<add name="RequestLengthCheck"
type="MyNamespace.RequestCheckModule, MyLibraryName" />
</httpModules>
</system.web>
Now you have to implement the HttpModule that would check request's size and redirect to an error page if it's too big:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;

namespace MyNamespace
{
public class RequestCheckModule : IHttpModule
{
public void Init(HttpApplication app)
{
app.BeginRequest += new EventHandler(app_BeginRequest);
}


void app_BeginRequest(object sender, EventArgs e)
{
HttpContext context = (HttpApplication)sender).Context;

if (context.Request.ContentLength > 4096000)
{
IServiceProvider prov = (IServiceProvider)context;
HttpWorkerRequest wr =
(HttpWorkerRequest)prov.GetService(typeof(HttpWorkerRequest));

// Check if body contains data
if (wr.HasEntityBody())
{
// get the total body length
int reqLength = wr.GetTotalEntityBodyLength();
// Get the initial bytes loaded
int initBytes = wr.GetPreloadedEntityBody().Length;

if (!wr.IsEntireEntityBodyIsPreloaded())
{
byte[] buffer = new byte[512000];
// Set the received bytes to initial
// byted before start reading
int recBytes = initialBytes;
while (reqLength - recBytes >= initBytes)
{
// Read another set of bytes
initBytes = wr.ReadEntityBody(buffer, buffer.Length);
// Update the received bytes
recBytes += initBytes;
}
wr.ReadEntityBody(buffer, reqLength-recBytes);
}
}
// Redirect the user to an error page.
context.Response.Redirect("Error.aspx");
}
}

public void Dispose()
{
}
}
}
I've found this piece of code here. It works perfect. The only thing I don't understand is why the whole request needs to be read for this to work? I've tried to make the redirection right after wr.GetTotalEntityBodyLength(); but it didn't work. Anybody knows why?

Disable all page elements with transparent div

Sometimes you want to disable all page elements at one time. There are many ways to achieve that. I'm using an additional div displayed on the top layer of the page. It covers the whole page content and makes clicking elements on lower layers impossible. The div can be totally transparent so the user still sees the content of the lower layers.

First, you need to place the additional div on your page, right behind the <body> tag:
...
</head>
<body>
<div id="disablingDiv" ></div>
...
Then, you need to create appropriate style for that div. This element needs to be displayed on the top layer:

#disablingDiv
{
/* Do not display it on entry */
display: none;

/* Display it on the layer with index 1001.
Make sure this is the highest z-index value
used by layers on that page */
z-index:1001;

/* make it cover the whole screen */
position: absolute;
top: 0%;
left: 0%;
width: 100%;
height: 100%;

/* make it white but fully transparent */
background-color: white;
opacity:.00;
filter: alpha(opacity=00);
}
Now it is ready to use but not displayed. If you want to enable the div (to disable all underlaying page elements) just invoke the following JS code:

document.getElementById('disablingDiv').style.display='block';
To disable it again:
   document.getElementById('disablingDiv').style.display='none';
You can also play with background color and opacity to create different effects. The div itself can contain some additional elements e.g. button allowing enabling, pictures, etc.
I've created this simple style basing on more complex example of lightbox described here.