Oct 06

Deploying Microsoft Chart Controls for Microsoft .NET Framework 3.5 is not supported in Visual Studio .NET Setup project by default due to unexistant boostrapper package (you cannot choose Chart Controls suite as prerequisite). In order to select chart package as prerquisite new boostrapper package should be created:

Step 1: Find bootrapper packages directory.
On 32bit environmenst it is located in
C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages
On 64bit environmenst it is located in
C:\Program Files (x86)\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages

Step 2: Create folder MS Chart Controls for NET Framwerk 35

Step 3: Copy mschart.exe to MS Chart Controls for NET Framwerk 35 folder.

Step 4: Create product.xml file and edit it

Step 5: Copy following content into it 
<?xml version="1.0" encoding="utf-8" ?>
<Product
 xmlns="
http://schemas.microsoft.com/developer/2004/01/bootstrapper"
 ProductCode="Microsoft.ChartControls"
>
 <PackageFiles CopyAllPackageFiles="false">
  <PackageFile
   Name="MSChart.exe"
   HomeSite="
http://download.microsoft.com/download/c/c/4/cc4dcac6-ea60-4868-a8e0-62a8510aa747/MSChart.exe" />
 </PackageFiles>

 <RelatedProducts>
  <DependsOnProduct
   Code="Microsoft.Net.Framework.3.5.SP1"
  />
 </RelatedProducts>

 <InstallChecks>
  <MsiProductCheck Property="ChartIsInstalled" Product="{41785C66-90F2-40CE-8CB5-1C94BFC97280}"/>
 </InstallChecks>
 
 <Commands>
  <Command PackageFile="MSChart.exe">
  
   <InstallConditions>
   
    <!-- ByPass if we have installed chart -->
        <BypassIf Property="ChartIsInstalled" Compare="ValueGreaterThan" Value="0" />
   
    <!-- Block install if user does not have admin privileges -->
        <FailIf Property="AdminUser" Compare="ValueEqualTo" Value="false" String="AdminRequired"/>
   
   </InstallConditions>
  
   <ExitCodes>
    <DefaultExitCode Result="Success" />
   </ExitCodes>
  </Command>
 </Commands>
</Product>

Step 6: Create subfolder named en

Step 7: Create file eula.txt and copy content from MSChart installation eula.

Step 8: Create file package.xml and copy following content into it

<?xml version="1.0" encoding="utf-8" ?>
<Package
  xmlns="
http://schemas.microsoft.com/developer/2004/01/bootstrapper"
  Name="DisplayName"
  Culture="Culture"
  LicenseAgreement="eula.txt"
>

    <PackageFiles>
        <PackageFile Name="eula.txt"/>
    </PackageFiles>

    <Strings>
        <String Name="DisplayName">Microsoft Chart Controls for .NET Framework 3.5</String>
        <String Name="Culture">en</String>
        <String Name="AdminRequired">Administrator permissions are required to install Microsoft Chart Controls for .NET Framework 3.5. Contact your administrator.</String>
        <String Name="AnotherInstanceRunning">Another instance of setup is already running. The running instance must complete before this setup can proceed.</String>
        <String Name="GeneralFailure">A failure occurred attempting to install Microsoft Chart Controls for .NET Framework 3.5.</String>
    </Strings>

</Package>

After implementing steps 1 through 8 MS Chart Controls will be available in Setup project prerequisites.

Tags:
Feb 13

Web form post requests with WCF services are supported in WCF 3.5. We can achieve this by using WebInvoke attribute and webHttpBinding. There are tons of blog posts, forum entries where you can find detailed info on this. My goal is to map post parameters (name-value pairs) to operation's parameters. Parameter matching is done by UriTemplate. Unfortunately this wasn't very useful in my case. A sample on MSDN demonstrates different approach of handling post arguments. In my opinion it is too complicated and not very suitable for common use (operation methods must have NameValueCollection parameter Undecided).

I just wanted to simplify things and made basic matching of post parameters with method parameters via their names. MSDN sample was my code base. Parameter matching can be accomplished with custom IDispatchMessageFormatter in endpoints behavior. Current version support only requests with no response. Main method for deserializing post request looks like:

/// 
/// Get http post stream, retrieve form variables and pass their values to operation method.
/// 
public void DeserializeRequest(Message message, object[] parameters)
{
	// Check content type
	if (WebOperationContext.Current.IncomingRequest.ContentType != "application/x-www-form-urlencoded")
	{
		throw new InvalidDataException("Unexpected content type");
	}

	// Form stream
	Stream stream = StreamMessageHelper.GetStream(message);

	// Form string
	string formData = new StreamReader(stream).ReadToEnd();

	// Form variables
	NameValueCollection formVariables = System.Web.HttpUtility.ParseQueryString(formData);

	// Operation methods parameters
	ParameterInfo[] operationParameters = operation.SyncMethod.GetParameters();

	// Match form variables with operation parameters
	QueryStringConverter converter = new QueryStringConverter();
	for (int i = 0; i < parameters.Length; i++)
	{
		string value = formVariables[operationParameters[i].Name];
		string name = operationParameters[i].Name;
		if (value != null)
		{
			if (converter.CanConvert(operationParameters[i].ParameterType))
				parameters[i] = converter.ConvertStringToValue(value, operationParameters[i].ParameterType);
			else
				parameters[i] = value;
		}
	}
}

Hard work is done. Now we need to write custom behavior which uses our new message formatter. Easiest way is to extend WebHttpBehavior and return our message formatter if operation contract has set WebInvoke attribute.

public class FormPostBehavior : WebHttpBehavior
{
	protected override IDispatchMessageFormatter GetRequestDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
	{
		//Messages[0] is the request message
		MessagePartDescriptionCollection parts = operationDescription.Messages[0].Body.Parts;

		//This formatter looks for [WebInvoke] operations
		if (operationDescription.Behaviors.Find() != null)
		{
			return new FormPostRequestFormatter(operationDescription);
		}
		else
		{
			return base.GetRequestDispatchFormatter(operationDescription, endpoint);
		}
	}
}

All you need to do is to add new behavior to endpoint behavior:

ServiceHost sh = new ServiceHost(typeof(TestFormPostService));
sh.Description.Endpoints[0].Behaviors.Add(new FormPostBehavior());

I don't like to define all service hosts parameters in code. I prefer to use configuration files. First we need to write new behavior element which creates our new behavior. It is simple:

public class FormPostBehaviorElement : BehaviorExtensionElement
{
	public override Type BehaviorType
	{
		get { return typeof(FormPostBehavior); }
	}

	protected override object CreateBehavior()
	{
		return new FormPostBehavior();
	}
}

To test this behavior just prepare html as:

<form id="form1" action="http://localhost:8001/TestService/Invoke" method="post" >
Parameter 1: <input type="text" name="parameter1" value="" /><br>
<input type="submit" name="submit" value="Test" />
</form>

Tags:
Jan 22

Building web projects using MSBuild and versioned by SubVersion a.k.a. SVN is quite challenging task Embarassed. On the end of process it's recommended to delete all .svn folders which are not mandatory for deployment. I thought this will be easy task to achieve, but I was wrong. MSBuild uses ItemGroup-s to recursively list files to user defined item. But you cannot recursively list directories to an item:

<ItemGroup>
  <
Files Include="$(TargetDir)\**\*"
/>
</
ItemGroup
>

I spend hours searching google and found "cool trick" to recursively delete folders:
1. Prepare item with all files
2. Prepare item with all folders -> extract root dir and directory from file item
3. Prepare item with .svn folders -> add ..\.svn to folder created at step 2. We need to go one level down because each folder may not contain files.
4. Remove folder created in step 3.
Final target for cleaning files look like: 

<Target Name="CleanTargetDir">
  <!-- 1. -->
  <
ItemGroup
>
    <
Files Include="$(TargetDir)\**\*"
/>
  </
ItemGroup
>
  <!-- 2. -->
  <
ItemGroup
>
    <
Folders Include="@(Files->'%(RootDir)%(Directory)')"
/>
  </
ItemGroup
>
  <!-- 3. -->
  <
ItemGroup
>
    <
FoldersToDelete Include="@(Folders->'%(RootDir)%(Directory)..\.svn')"
/>
  </
ItemGroup
>
  <!-- 4. -->
 
<RemoveDir Directories="@(FoldersToDelete)"/>
</
Target>

 If anyone have better solution I'll be glad to hear it.

 

Tags:
Jan 18

Lately I was trying to automate process of assigning assembly version for all my projects in solution. My tool of choice for build is MSBuild where is no appropriate support for file operations nor assembly version handling. I downloaded and installed MSBuild Community Task extensions which can be found at http://msbuildtasks.tigris.org/. Extension pack has FileUpdate task where you can find and replace specific text using regular expressions.

Build configuration file for assigning assembly version looks like:

<?xml version="1.0" encoding="utf-8"?>
<
Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
>
  <
Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"
/>
  <
PropertyGroup
>
    <
Major>3</Major
>
    <
Minor>5</Minor
>
    <
Build>1</Build
>
    <
Revision>0</Revision
>
  </
PropertyGroup
>
  <
ItemGroup
>
    <
AssemblyFile Include="Project1Folder\Properties\AssemblyInfo.cs"
/>
    <
AssemblyFile Include="Project2Folder\Properties\AssemblyInfo.cs"
/>
  </
ItemGroup
>
  <
Target Name="VersionUpdate"
>
    <
Message Text="Version: $(Major).$(Minor).$(Build).$(Revision)"
/>
   
<FileUpdate Files="@(AssemblyFile)"
        Regex='AssemblyVersion\("(\d+)\.(\d+)\.(\d+)\.(\d+)"\)
'
        ReplacementText='AssemblyVersion("$(Major).$(Minor).$(Build).$(Revision)")
'
        Condition=""
/>
    <
FileUpdate Files="@(AssemblyFile)
"
        Regex='AssemblyFileVersion\("(\d+)\.(\d+)\.(\d+)\.(\d+)"\)
'
        ReplacementText='AssemblyFileVersion("$(Major).$(Minor).$(Build).$(Revision)")
'
        Condition=""
/>
  </
Target
>
</
Project>

Tags:
Jan 11

Existing RequiredFieldValidator cannot validate CheckBoxList nor RadioButtonList control. Instead it raises exception: "Control 'checkBoxList' referenced by the ControlToValidate property of 'requiredFieldValidator' cannot be validated.". Writting custom validator is quite simple process. There is one important thing to be aware of. CustomValidator performs validation if controls Text property is set. Problem is that CheckBoxList and RadioButtonList Text property is always empty, Therefore we should first set validotars ValidateEmptyText property to true.

JavaScript to perform client side validation looks like:

function MyCode_ListControlRequiredValidation(source, args) {
	var listObj = document.getElementById(source.controltovalidate);
	var listObjInputs = listObj.getElementsByTagName('input');
	for(var i = 0; i < listObjInputs.length; i++) {
		if (listObjInputs[i].checked) {
			args.IsValid = true;
			return;
		}
	}
	args.IsValid = false;
}

For server side validation we should override ControlPropertiesValid and EvaluateIsValid methods. Detailed description can be found on MSDN site.

ListControlRequiredValidator.cs (1.61 kb)

Tags: |
Jan 09

Probably everybody have tried to use asp menu control. Control can be oriented in two ways:
- horizontal where dynamic content is expanded downwards
- vertical where dynamic content is expanded in right direction
Recently I've placed menu on a bottom of a page and wanted to expand upwards. None of the properties allows that kind of behaviour, I needed quick solution for the problem. Most of solutions found on net involves rewriting javascript method PopOut_Show. I don't want to mess with that code so I decided to write own expansion.

Let's look how menu control works. Static menu content is rendered with classic html table element. Dynamic content is placed in div element. When user moves mouse over table's cell then an iframe element is created and populated with dynamic content. To achieve upward expansion of menu we need to move up div and iframe elements. If we move up only one of those elements a region of white space shows up on a screen.

First thing we need to do is to find all static containers and change onmouseover event. These containers are html TD cells named {0}n{1} where {0} is menu ClientID and {1} is a sequential number starting with 0. We need to call existing method and then make correction of DIV (named {0}n{1}Items) and IFRAME (named {0}n{1}_MenuIFrame) element. Javascript which changes mouseover event looks like:

function MyCode_AspMenuUpwardExpandInit(menuId) {
	var i = -1;
	var menuTdObj = null; 
	do {
		i++;
		menuTdId = menuId + 'n' + i;
		menuTdObj = WebForm_GetElementById(menuTdId);
		if (menuTdObj != null) {
			var mouseOverScript = menuTdObj.onmouseover + '';
			if (mouseOverScript.indexOf('Dynamic') < 0) {
				mouseOverScript = mouseOverScript.substring(mouseOverScript.indexOf('{') + 1, mouseOverScript.lastIndexOf('}'));
				var panelId = menuTdId + 'Items';
				var script = mouseOverScript + '; MyCode_AspMenuUpwardExpandCorrection(\'' + menuTdId + '\', \'' + panelId + '\');';
				menuTdObj.onmouseover = new Function(script);
			}
		}
	}
	while(menuTdObj != null)
}

Javascript which moves up div panel and iframe:

function MyCode_AspMenuUpwardExpandCorrection(menuTdId, panelId) {
	var panel = WebForm_GetElementById(panelId);
	if (panel == null) return;
	var panelPos = WebForm_GetElementPosition(panel);

	var menuTdObj = WebForm_GetElementById(menuTdId);
	var menuTdObjPos = WebForm_GetElementPosition(menuTdObj);

	var childFrameId = panelId + '_MenuIFrame';
	var childFrame = WebForm_GetElementById(childFrameId);

	var newY = menuTdObjPos.y - panelPos.height;

	WebForm_SetElementY(panel, newY);
	WebForm_SetElementY(childFrame, newY);
}

To use this we need to call MyCode_AspMenuUpwardExpandInit function and pass it menu's ClientID property

<script type="text/javascript" language="javascript">
MyCode_AspMenuUpwardExpandInit(<%= cmsMenu.ClientID %>);
</script>

or you can use webcontrol (AspMenuUpwardExpand.cs (2.58 kb)):

<asp:Menu runat="server" ID="cmsMenu" Orientation="Horizontal" DataSourceID="xmlDataSource">
</asp:Menu>
<
MyCode:AspMenuUpwardExpand ID="menuUpward" runat="server" MenuControl="cmsMenu" />

This solution works in IE8, FF3, Chrome. Didn't have time to check other browsers.

Tags: |