Thursday, April 29, 2010

WCF, wsHttpBinding and userName authentication

Ok, before we begin here is what you need: a website with SSL properly configured using a certificate with a root certificate. You need to install the root certificate on the client from which you are going to be accessing the web-service.

SSL is required because WCF will not allow you to send username/password over an insecure channel. WCF is stringent about this and hence it doesnt even allow a plain self-signed certificate. The cert needs to have a root that is part of the trusted authority certs.

Without a root certificate, you will get the following error:

Could not establish trust relationship for the SSL/TLS secure channel with authority 'fullyQualifiedDomainName'.

For instructions on creating a website cert that has a root cert, see my previous post: Using MakeCert to create a certificate with a trusted root for an IIS website

1. Create a WCF Service website

image

2. This will create IService.cs (the service contract), Service.cs (the service implementation) and Service.svc (the webservice access point). We will be using the default implementation to for this tutorial.

3. Create the UserNameValidator class

Add a class to the app_code folder called UserNameValidator

Add the following code to the file

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;

/// <summary>
/// Summary description for UserNameValidator
/// </summary>
public class UsernameValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        // validate arguments
        if (string.IsNullOrEmpty(userName))
            throw new ArgumentNullException("userName");
        if (string.IsNullOrEmpty(password))
            throw new ArgumentNullException("password");

        // check if the user is not test
        if (userName != "test" || password != "test")
            throw new SecurityTokenException("Unknown username or password");
    }
}

This code inherits from the UserNamePasswordValidator class and checks for the username “test” and password “test”.

4. First a simple test

Lets test the service using basicHttpBinding

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="basicHttpBinding" bindingConfiguration="basic" name="basicService" contract="IService"/>
             </service>
        </services>
    </system.serviceModel>

The above configuration enables you to access the service using basicHttpBinding.

Once you have made sure that the service works, move on to the next step. (use WcfTestClient.exe to help you testing the service – "C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe")

5. Convert the webservice to use wsHttpBinding without SSL.

For this you need to change your configuration as follows:

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
           
<wsHttpBinding>
                <binding name="wsPlainBinding"></binding>
            </wsHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsPlainBinding" name="wsPlainService" contract="IService"/>
            </service>
        </services>
    </system.serviceModel>

6. Migrate your site into IIS.

Visual Stuido’s web server (Casini) does not support ssl (https), so you need to move your into IIS.

Easiest way to do this is to create a virtual folder that points to your development folder.

Once that is done, test your webservice once again, this time running it from within IIS. (you will also need to change the settings in VS so that it uses IIS for debugging).

image

7. Update your configuration to use the UserNameValidator class as the custom validator

Here is the updated configuration:

<system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="defaultProfile">
                    <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
                   
<serviceCredentials>
                        <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="UsernameValidator, App_Code"/>
                    </serviceCredentials>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
            <wsHttpBinding>
               
<binding name="wsSecureBinding">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName"/>
                    </security>
                </binding>
        <binding name="wsPlainBinding"></binding>
      </wsHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="defaultProfile" name="Service">
                <endpoint address="" binding="wsHttpBinding" bindingConfiguration="wsSecureBinding" name="wsSecureService" contract="IService"/>
            </service>
        </services>
    </system.serviceModel>

You should be able to view your service over https.

The only way to test the service now is to create a console app with a service reference to the https endpoint for your service. The code in your client will look like this

using (ServiceReference1.ServiceClient sc = new ServiceReference1.ServiceClient())
            {
                sc.ClientCredentials.UserName.UserName = "test";
                sc.ClientCredentials.UserName.Password = "test";
                string returndata = sc.GetData(100);
            }

8. Bringing it all together

Here is a reference configuration that brings all of the above configurations together, by providing multiple end-points:

/basic – WS-1, no security
/wsPlainBinding– WS-* no authentication
/wsSecureNoUserNameBinding – WS-* – transport security, no authentication
/wsSecureService– WS-* – transport security with authentication

<system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="defaultProfile">
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
              includeWindowsGroups="false" customUserNamePasswordValidatorType="UsernameValidator, App_Code" />
            <windowsAuthentication includeWindowsGroups="false" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <basicHttpBinding>
        <binding name="basic"></binding>
      </basicHttpBinding>
      <wsHttpBinding>
        <binding name="wsSecureBinding">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None" />
            <message clientCredentialType="UserName" negotiateServiceCredential="false" establishSecurityContext="false"/>
          </security>
        </binding>
        <binding name="wsSecureNoUserNameBinding">
          <security mode="Transport">
                <transport clientCredentialType="None" />
                <message clientCredentialType="None" negotiateServiceCredential="false" establishSecurityContext="false" />
            </security>
        </binding>
        <binding name="wsPlainBinding">
            <security mode="None">
                <transport clientCredentialType="None" />
                <message clientCredentialType="None" negotiateServiceCredential="false" establishSecurityContext="false" />
            </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="defaultProfile" name="Service">
        <endpoint address="/basic" binding="basicHttpBinding" bindingConfiguration="basic"
          name="insecureService" contract="IService" />
        <endpoint address="/wsPlainBinding" binding="wsHttpBinding" bindingConfiguration="wsPlainBinding"
          name="wsService" contract="IService" />
        <endpoint address="/wsSecureNoUserNameBinding" binding="wsHttpBinding"
          bindingConfiguration="wsSecureNoUserNameBinding" name="wsSemiSecureService"
          contract="IService" />
        <endpoint address="/wsSecureService" binding="wsHttpBinding"
          bindingConfiguration="wsSecureBinding" name="wsSecureService"
          contract="IService" />
      </service>
    </services>
  </system.serviceModel>

Using MakeCert to create a certificate with a trusted root for an IIS website

Why? If you dont use a certificate with a trusted root, then web browsers will complain that there is something wrong with the certificate. So what you need to do is to create both the Certificate Authority certificate (the root) and then create a derived certificate that you can use on your website. You then need to install the root certificate on all the clients that will access the website. Once that is done, when the clients access your site over SSL (https), they will get the lock icon and no error messages.

The most important use of this configuration is when you need to use username authentication with WCF using wsHttpBinding. (WCF will not allow you to use this configuration over an invalidly configured SSL connection). A separate post will detail how to enable username authentication using wsHttpBinding in WCF.

Note: this should only be used for development/testing. Never for production

1. Make a Certificate Authority certificate (root certificate) that is installed to “Trusted Root Certification Authorities”

makecert -r -pe -n "CN=Raj Local Certificate Root" -ss Root -sr localMachine -a sha1 -sky signature -sv c:\certs\RajRoot.pvk c:\certs\RajRoot.cer

2. Check that the cert was installed to “Trusted Root Certification Authorities”

To do this you need the Certificates admin tool. The easy way to do this is
Start > Run > MMC
File > Add-Remove Snap-In
Click Add
Select Certificates and click Add
Select Computer Account and hit Next
Select Local Computer

The CA certificate (created in step 1) is the one that you will have to put on all the clients that will access the SSL website. (otherwise you will end up getting an error that the certificate is not trusted).

3. Create a certificate using the CA cert. This is the one that you will install to your web server

makecert -pe -n "CN=fq.dn" -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -ic c:\certs\RajRoot.cer -iv c:\certs\RajRoot.pvk -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 -sv c:\certs\rajServer.pvk c:\certs\rajServer.cer 
pvk2pfx -pvk c:\certs\rajServer.pvk -spc c:\certs\rajServer.cer -pfx c:\certs\rajServer.pfx

In the certificates admin tool, right click > All Tasks > Import and import the pfx file that you created above.

4. Assign the certificate to your IIS website.

Right click on the website > Properties > Directory Security tab
Server Certificate
On the second page choose “Assign an existing certificate”
Select the certificate that you imported in the last step of (3).

5. Test

Try and browse to a valid page on your site. Next try and add the s to the http and try it again and make sure you are able to connect to the site.

Notes:

Using MakeCert
http://www.digitallycreated.net/Blog/38/using-makecert-to-create-certificates-for-development (a good post that details all the different options)

Creating X.509 Certificates using makecert.exe: http://blogs.microsoft.co.il/blogs/applisec/archive/2008/04/08/creating-x-509-certificates-using-makecert-exe.aspx

Wednesday, April 28, 2010

Setting up WCF Configuration using the Service Configuration Editor

1. Create a new service using basicHttpBinding

Service type: Typically the name of the class that will be implementing the Service contract (interface).
Contract: Name of the interface that represents the service contract that this service is using.
Communication mode: select HTTP if your service will be running within IIS as a website.
Interoperability: Basic web services interoperability (for BasicHttpBinding)
Address: (blank)

Once the wizard completes, click on the “Services” node under Configuration and then click on the first node under EndPoints (typically - (Empty Name)).
Provide a name for the endpoint. (basic)
Click on the “Services” node. Under the services pane, click “Click to create” for Binding Configuration.
Change the name to BasicBinding.

Add WSDL
Go to the node “Service behaviors” under advanced.
Click “New service behavior configuration” and name it BasicServiceBehavior
Click Add, and select “serviceMetadata”
Double click the added item “serviceMetaData”.
Set HttpGetEnabled to true.
Click on the “Service” node under “Services” (this is the service that you have created)
Set the behavior configuration to the one you just created.

This is what your configuration will look like

<behaviors>
   <serviceBehaviors>
    <behavior name="BasicServiceBehavior">
     <serviceMetadata httpGetEnabled="true" />
    </behavior>
   </serviceBehaviors>
  </behaviors>
  <bindings>
   <basicHttpBinding>
    <binding name="BasicBinding" />
   </basicHttpBinding>
  </bindings>
  <services>
   <service behaviorConfiguration="BasicServiceBehavior" name="Service">
    <endpoint address="" binding="basicHttpBinding" bindingConfiguration="BasicBinding"
     name="basic" contract="IService" />
   </service>
  </services>

2. Create an endpoint that uses wsHttpBinding

First – create your binding.
Right click on “Bindings” node and select new binding configuration.
Name it “wsInsecureBinding”
On the security tab (right pane), set Mode to none, EstablishSecurityContext to false, MessageClientCredentialType to None, NegotiateServiceCredential to false, TransportClientCredentialType to None.
image 
Now create the endpoint:

Right click on the EndPoints node (under Services->YourServiceName) and select new endpoint. Name it “wsInsecure”
Set the address to /wsInsecure
Set the binding to wsHttpBinding
Set the bindingConfiguration to the one you just created “wsInsecureBinding”
Set the contract to the name of the interface that is your service contract.
image

The binding configuration will now look like this (changes highlighted in bold):

<behaviors>
   <serviceBehaviors>
    <behavior name="BasicServiceBehavior">
     <serviceMetadata httpGetEnabled="true" />
    </behavior>
   </serviceBehaviors>
  </behaviors>
  <bindings>
   <basicHttpBinding>
    <binding name="BasicBinding" />
   </basicHttpBinding>
   <wsHttpBinding>
    <binding name="wsInsecureBinding">
     <security mode="None">
      <transport clientCredentialType="None" />
      <message clientCredentialType="None" negotiateServiceCredential="false"
       establishSecurityContext="false" />
     </security>
    </binding>
   </wsHttpBinding>
  </bindings>
  <services>
   <service behaviorConfiguration="BasicServiceBehavior" name="Service">
    <endpoint address="" binding="basicHttpBinding" bindingConfiguration="BasicBinding"
     name="basic" contract="IService" />
   <endpoint address="/wsInsecure" binding="wsHttpBinding" bindingConfiguration="wsInsecureBinding"
     name="wsInsecureBinding" contract="IService" />
   </service>
  </services>

WCF and SoapUI – Error importing service

If SoapUI is unable to import your webservice, check the URI. SoapUI does not like spaces in the URI.

Tuesday, April 27, 2010

TFS 2008 Build Server and VS 2010

Had to install VS 2010 on the TFS 2008 build server machine, so that I could get continuous integration with VS 2010 solutions. (We are not moving to TFS 2010 just yet as we have a dependency on Office 2003).

After having installed VS 2010 on the build server all CI builds were failing. First with the error:

Error MSB4131: The "Reason" parameter is not supported by the "GetBuildProperties" task. Verify the parameter exists on the task, and it is a gettable public instance property.

The simple work-around/hack for this is to open the “C:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets” file and comment out the line

<Output TaskParameter="Reason" PropertyName="Reason" />

Once you do this, you will come across another error:

error MSB4018: The "CreateWorkspaceTask" task failed unexpectedly.

Turns out this is because VS 2010 modifies the Build.Targets file’s Workspace entry to the following:

<WorkspaceName Condition=" '$(WorkspaceName)'=='' " >$(COMPUTERNAME)_$(BuildDefinitionId)_$(BuildAgentId)</WorkspaceName>

TFS 2008 does not like the _$(BuildAgentId). So if you set the value to $(COMPUTERNAME)_$(BuildDefinitionId) life should get back to normal for you.

Finally, if you need to VS2010 to work with your build server, edit the “C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\tfsbuildservice.exe.config” file and set the MsBuildPath.

<add key="MSBuildPath" value="C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\" />

Monday, April 26, 2010

TFS 2010 and Office 2003

In short: doesnt work! – TFS 2010 requires Office 2007 for managing your project.

Here is a hack that allows you to continue working with Office 2003 with TFS 2010. It is a hack because you cannot create a new file from VS 2010. The only way to create a new excel document linked to your project is from VS 2008. In addition there are some other issues, such as no support for tree of items, etc.

Here are the steps: http://blogs.msdn.com/team_foundation/archive/2010/04/24/vs-2010-and-tfs-with-office-2003.aspx

VS 2010 Code Analysis Issue

Are you getting the following error when Code Analysis is run on a project with Visual Studio 2010?

MSBUILD : error : CA0059 : Invalid settings passed to CodeAnalysis task

For me this was being caused because I had run FxCop on my machine. This lead to an environment variable called “FxCopDir” being created and that pointed to the FxCop install directory. Once I deleted the FxCopDir environment variable and restarted Visual studio, Code Analysis began running properly.

To get to the environment variables, right click and select properties on “My Computer”, On the Advanced tab you will find the “Environment Variables” button. Find and delete FxCopDir variable.

Updating the TFS Warehouse cube

The TFS warehouse cube is updated on a schedule (typically hourly). But if you need to update the cube ad-hoc, here is what you do:

1. Goto http://teamserver:8080/Warehouse/v1.0/WarehouseController.asmx

2. Run the “Run” operation. You should get a return value of true

3. Run the “GetWarehouseStatus” operation to determine if the update completed (will return Idle).

The invoke operation is available only on the server where TFS is running.

Saturday, April 24, 2010

Friday, April 23, 2010

Windows Phone Developer Tools CTP and Visual Studio 2010 RTM

VS 2010 RTM is not compatible with Windows Phone Developer Tools CTP. This is because the phone dev tools are built upon VS 2010 RC and the changes to VS 2010 RTM have made the 2 incompatible.

You can register to be notified of when a VS2010 RTM compatible version is released at this site: http://www.microsoft.com/windowsmobile/en-us/cmpn/vslaunch/default.mspx

Also note: the phone dev tools will not install to a VPC image.

Thursday, April 22, 2010

IIdentity, IPrincipal whats the difference?

IIdentity represents the user.

IPrincipal represents the user along with all his role memberships.

This is why IPrincipal has a reference to the IIdentity object that it encapsulates. In addition it also have methods to determine role membership.

Wednesday, April 21, 2010

Sharepoint Site Permissions

Changing site collection administrators:

- Central Administration > Application Management > Site Collection Administrators

- Choose the site for which you want to change the administrator for. (the sites can be chosen from the drop down box called “Site Collection”)

- Once you have chosen the correct site, set the “Primary site collection administrator” and “Secondary site collection administrator” (you cant set this to a group)

Add users to the site:

You need to be a site collection admin or a user with appropriate rights for the site, to be able to add/remove users to a site. (see above).

Once you have sufficient privileges to access the site go to the site.

- Site Actions > Site Settings (this is drop down on the right side of the site)

- Under “Users and Permissions” select “People and Groups”

- Click New and Add User or Add Group and add the user to the site

- Set the correct permissions for the user

The above steps are useful when you need to change access to a TFS Team Project Site and for some reason the TFS administration console does not work for you. (Happened to me after upgrading to TFS2010 and the migrated site would not show up in TFS Administration Tool)

Monday, April 19, 2010

YAGNI, YAGNI, YAGNI…

You aint gonna need it:

"Always implement things when you actually need them, never when you just foresee that you need them."

http://c2.com/xp/YouArentGonnaNeedIt.html

Saturday, April 17, 2010

TFS 2010 Upgrade Problems – Missing Team Project Portals (Scrum Template)

In my previous post (Upgrade to TFS 2010 via migration – All Green!!!), I had written about how I had successfully upgrade through migration our TFS 2008 installation to a 2010 installation. After a little more checking I found this was not entirely true; some of the Team Project portals dont come up!

It looks like the one project that I have that used the MSF Agile template comes up correctly. But all the other ones that use the Conchango v2 Scrum template dont come up. (I get a 404 error). I am trying to find a solution and will blog about it if I find one. If you have had a similar problem – please leave a comment…. I am at wit’s end… having spent almost 2 days trying to get the upgrade to TFS 2010 to work.

Upgrade to TFS 2010 via migration – All Green!!!

After almost 16 hours of non-stop fiddling with all the installation settings and configurations…. I got the all green page on my TFS migration.

image

I plan on posting the in-depth steps that I took later, but here is a basic overview:

0. Backup all the databases from your old server

1. Installed SQL Server 2008 Standard

2. Installed WSS 3.0 SP2

WSS 3.0 only setup a the SharePoint Central Administration Site (SCAS)

I had to create a new Web Application

- SCAS –> Application Management –> “Create or Extend Web Application”

- Created a web application, with a new IIS site called “Default Web Application” with port set to 80. Also set it to use the DefaultAppPool. Left all the other settings at their default.

- Once the site came up, I restored the WSS_Content database from the old server to the new SQL Server. Once that was done, I attached the content database to the new site (web-application in WSS terminology)

- Attempted to visit one of the sites – but it did not work…. I was hoping maybe a TFS component was not installed and that was why the site was not working… so I planned on returning to this later

3. Opened up Reporting Services Configuration Console

- Restored the 2 reporting databases from the old server and then attached them to the reporting server

- Under Encryption Keys, I needed to delete the keys and reset all the account information

- visited the site to make sure everything came up.

4. Restored all 7 TFS databases from the old server onto the new server

5. Installed TFS2010 Server

6. Opened up Sql Server Management Studio and logged into the Analysis Server component and deleted the TFSAnalysis database.

6. Configuration dialog for TFS2010 server came up.

- Under configuration: chose the Upgrade option

- Got green on the verification.

- ran the installer

- got green on the completion.

Unfortunaltely…. the only thing not working for me right now…. is the Sharepoint portal for each of my Team Projects…. all other parts seem to be working correctly.

More to follow.

Wednesday, April 14, 2010

TFS 2008 Database Backup and Restore scripts

These scripts can come handy when performing a migration based upgrade.

Backup Script

BACKUP DATABASE [ReportServer] TO  DISK = N'E:\Backups\ReportServer.bak' WITH NOFORMAT, NOINIT,  NAME = N'ReportServer-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [ReportServerTempDB] TO  DISK = N'E:\Backups\ReportServerTempDB.bak' WITH NOFORMAT, NOINIT,  NAME = N'ReportServerTempDB-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsActivityLogging] TO  DISK = N'E:\Backups\TfsActivityLogging.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsActivityLogging-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsBuild] TO  DISK = N'E:\Backups\TfsBuild.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsBuild-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsIntegration] TO  DISK = N'E:\Backups\TfsIntegration.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsIntegration-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsVersionControl] TO  DISK = N'E:\Backups\TfsVersionControl.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsVersionControl-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsWarehouse] TO  DISK = N'E:\Backups\TfsWarehouse.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsWarehouse-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsWorkItemTracking] TO  DISK = N'E:\Backups\TfsWorkItemTracking.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsWorkItemTracking-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [TfsWorkItemTrackingAttachments] TO  DISK = N'E:\Backups\TfsWorkItemTrackingAttachments.bak' WITH NOFORMAT, NOINIT,  NAME = N'TfsWorkItemTrackingAttachments-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [WSS_AdminContent] TO  DISK = N'E:\Backups\WSS_AdminContent.bak' WITH NOFORMAT, NOINIT,  NAME = N'WSS_AdminContent-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [WSS_Config] TO  DISK = N'E:\Backups\WSS_Config.bak' WITH NOFORMAT, NOINIT,  NAME = N'WSS_Config-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [WSS_Content] TO  DISK = N'E:\Backups\WSS_Content.bak' WITH NOFORMAT, NOINIT,  NAME = N'WSS_Content-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

Restore Script

RESTORE DATABASE [ReportServer] FROM  DISK = N'E:\Backups\ReportServer.bak' WITH  FILE = 1,  MOVE N'ReportServer' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\ReportServer.mdf',  MOVE N'ReportServer_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\ReportServer.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [ReportServerTempDB] FROM  DISK = N'E:\Backups\ReportServerTempDB.bak' WITH  FILE = 1,  MOVE N'ReportServerTempDB' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\ReportServerTempDB.mdf',  MOVE N'ReportServerTempDB_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\ReportServerTempDB_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsActivityLogging] FROM  DISK = N'E:\Backups\TfsActivityLogging.bak' WITH  FILE = 1,  MOVE N'TfsActivityLogging' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsActivityLogging.mdf',  MOVE N'TfsActivityLogging_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsActivityLogging_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsBuild] FROM  DISK = N'E:\Backups\TfsBuild.bak' WITH  FILE = 1,  MOVE N'TfsBuild' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsBuild.mdf',  MOVE N'TfsBuild_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsBuild_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsIntegration] FROM  DISK = N'E:\Backups\TfsIntegration.bak' WITH  FILE = 1,  MOVE N'TfsIntegration' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsIntegration.mdf',  MOVE N'TfsIntegration_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsIntegration_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsVersionControl] FROM  DISK = N'E:\Backups\TfsVersionControl.bak' WITH  FILE = 1,  MOVE N'TfsVersionControl' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsVersionControl.mdf',  MOVE N'TfsVersionControl_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsVersionControl_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsWarehouse] FROM  DISK = N'E:\Backups\TfsWarehouse.bak' WITH  FILE = 1,  MOVE N'TfsWarehouse' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWarehouse.mdf',  MOVE N'TfsWarehouse_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWarehouse.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsWorkItemTracking] FROM  DISK = N'E:\Backups\TfsWorkItemTracking.bak' WITH  FILE = 1,  MOVE N'TfsWorkItemTracking' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWorkItemTracking.mdf',  MOVE N'TfsWorkItemTracking_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWorkItemTracking.LDF',  MOVE N'sysft_TeamFoundationServer10FullTextCatalog' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWorkItemTracking.TeamFoundationServer10FullTextCatalog',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [TfsWorkItemTrackingAttachments] FROM  DISK = N'E:\Backups\TfsWorkItemTrackingAttachments.bak' WITH  FILE = 1,  MOVE N'TfsWorkItemTrackingAttachments' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWorkItemTrackingAttachments.mdf',  MOVE N'TfsWorkItemTrackingAttachments_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TfsWorkItemTrackingAttachments_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [WSS_AdminContent] FROM  DISK = N'E:\Backups\WSS_AdminContent.bak' WITH  FILE = 1,  MOVE N'WSS_AdminContent' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_AdminContent.mdf',  MOVE N'WSS_AdminContent_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_AdminContent_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [WSS_Config] FROM  DISK = N'E:\Backups\WSS_Config.bak' WITH  FILE = 1,  MOVE N'WSS_Config' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_Config.mdf',  MOVE N'WSS_Config_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_Config_1.LDF',  NOUNLOAD,  STATS = 10
GO

RESTORE DATABASE [WSS_Content] FROM  DISK = N'E:\Backups\WSS_Content.bak' WITH  FILE = 1,  MOVE N'WSS_Content' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_Content.mdf',  MOVE N'WSS_Content_log' TO N'E:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\WSS_Content_1.LDF',  NOUNLOAD,  STATS = 10
GO

Tuesday, April 13, 2010

'PageMethods' is undefined

“Microsoft JScript runtime error: 'PageMethods' is undefined”

I needed to call a server side method from my ASP.Net AJAX page.

The easy way to do this is to set the EnablePageMethods="true" on the ScriptManager object and then to add a static public method in the code behind file with the attribute: “[System.Web.Services.WebMethod]”

Once I had my simple method working, I tried to refactor my code by moving all the items into a user control…. and suddenly everything stopped working!. The error I was getting is the “PageMethods is undefined”.

All posts pointed to check if the EnablePageMethods was defined on the ScriptManager and that the “[System.Web.Services.WebMethod]” attribute was defined on the method. And they were. The code was all working fine when it was in a single aspx page. My problems started only when I moved the code into a separate ascx user control.

Turns out the problem was that PageMethods was unable to find the public static method that was being called from javascript. The simple fix was for the static method to be moved to the host page’s code-behind file. Once I did that, the issue went away.

Note: the javascript can sit in the user-control. It is only the WebMethod that needs to be moved to the host page.

Now the problem obviously becomes – how do you reuse the user control when the host page is where the code needs to reside? I am not sure there is any way around this dependency. (though, if you hate it – then maybe converting the method to a webservice is a work around. In my case, all I needed was this one method and that was all). Still looking into this usercontrol issue with WebMethods and will update this post if I find out anything else.

Backup script for TFS 2008 databases

Change the location of the backups before running the script.

BACKUP DATABASE [ReportServer] TO  DISK = N'E:\New Folder\ReportServer.bak' WITH NOFORMAT, NOINIT,  NAME = N'ReportServer-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10
GO

BACKUP DATABASE [ReportServerTempDB] TO DISK = N'E:\New Folder\ReportServerTempDB.bak' WITH NOFORMAT, NOINIT, NAME = N'ReportServerTempDB-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsActivityLogging] TO DISK = N'E:\New Folder\TfsActivityLogging.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsActivityLogging-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsBuild] TO DISK = N'E:\New Folder\TfsBuild.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsBuild-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsIntegration] TO DISK = N'E:\New Folder\TfsIntegration.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsIntegration-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsVersionControl] TO DISK = N'E:\New Folder\TfsVersionControl.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsVersionControl-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsWarehouse] TO DISK = N'E:\New Folder\TfsWarehouse.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsWarehouse-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsWorkItemTracking] TO DISK = N'E:\New Folder\TfsWorkItemTracking.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsWorkItemTracking-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [TfsWorkItemTrackingAttachments] TO DISK = N'E:\New Folder\TfsWorkItemTrackingAttachments.bak' WITH NOFORMAT, NOINIT, NAME = N'TfsWorkItemTrackingAttachments-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [WSS_AdminContent] TO DISK = N'E:\New Folder\WSS_AdminContent.bak' WITH NOFORMAT, NOINIT, NAME = N'WSS_AdminContent-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [WSS_Config] TO DISK = N'E:\New Folder\WSS_Config.bak' WITH NOFORMAT, NOINIT, NAME = N'WSS_Config-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

BACKUP DATABASE [WSS_Content] TO DISK = N'E:\New Folder\WSS_Content.bak' WITH NOFORMAT, NOINIT, NAME = N'WSS_Content-Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
GO

Monday, April 12, 2010

What every .Net developer ought to know about JavaScript

  1. Every JavaScript object is a dictionary.
  2. Every JavaScript function is an object.
  3. Every JavaScript object references a prototype object.

A good post by Scott Allen : http://odetocode.com/Articles/473.aspx

ASP:Label within UpdatePanel does not update when referenced from JavaScript

If you try and update the value of an ASP:Label element from client side javascript code and the label is nested within an ASP:UpdatePanel, then the problem might be the fact a post-back event is resetting the value of the label.

Here is what I had: An asp:UpdatePanel with an asp:label and an asp:button. When the asp:button was clicked a client side script would run and needed to update the value of the label.

Everytime my client event fired and changed the InnerHtml of the label, the page did not show the change.

The problem was that the asp:button was causing an update of the updatepanel which then updated the value of the label from data in the view state.

Here are 2 solutions:

In this case, I should have just used a plain old HTML button (<input type="button" …> ) and this would have not caused the update panel to get updated. (Also, I could have used a simple div element instead of an ASP:Label – but I was also using the label for server side messaging).

But what if you needed the button to perform some server side logic? In this case you could set the UpdateMode of the UpdatePanel to “Conditional” and set the ChildrenAsTriggers property to “false”. Both of these will not allow the update panel to refresh after a post back, thereby not overwriting the values of the label.

ASP.Net: Referencing root-relative paths (~/) in javascript

The “~/” specifies a root-relative path in ASP.Net. ASP.Net uses the ResolveUrl and ResolveClientUrl to determine the logical path that a path containing the “~/” points to.

If you need to use a root relative path from within a JavaScript method running within an ASP.Net page, then here is what the code looks like:

<script type="text/javascript">
    function OnSuccess() {
              window.open('<%=Page.ResolveClientUrl("~/newWindow.aspx")%>', 
                'mywindow', 
                'width=400,height=200,toolbar=yes,location=yes,directories=yes,status=yes,menubar=yes,scrollbars=yes,copyhistory=yes,resizable=yes');
      }
</script>

And here is a useful site with information on ASP.Net Paths - http://www.west-wind.com/weblog/posts/132081.aspx

Give me parameterized SQL, or give me death

An old post by Jeff Atwood – but a timeless one that every developer should read over and over again until it becomes second nature.

http://www.codinghorror.com/blog/2005/04/give-me-parameterized-sql-or-give-me-death.html

Also read the sister post “Who needs stored procs, Anyways

Thursday, April 08, 2010

Impersonating a specific windows identity

Alternate Title: How to get a WCF web service to use an alternate fixed identity to access a resource (such as a database).

By default, WCF will ignore the “<identity impersonate="true" userName="domain\username" password="xxxxxxx" />”. This is because even though the WCF service and ASP.net web site are using the same web.config, the ASP.net process is using only the settings under the System.Web node for its configuration and the WCF service is using the System.ServiceModel node for its configuration.

So how do you get WCF to use a particular identity to access a resource such as a database?

The problem I had was that I had a public facing website which needed to access a database. The database allowed only for windows authentication. Everytime the website called the webservice, the connection.Open command would fail as the user accessing the database was invalid.

There are a few ways to get around this issue.

1. Use the correct Identity as the ASPNet app pool identity. I believe this is the identity that WCF will use when attempting to access any resource. Unfortunately, if you are using IIS under XP then you dont have access to the AppPool identity. (which was my case). Using the correct identity is the best method to enable the scenario we are working on here.

2. If (1) is not possible, then you can get WCF to accept your “identity impersonate” settings that have been set in the system.web node.

The first thing to do is add the following line under the <system.servicemodel> node of your web.config.

<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

As you can see, what we are specifying here is that WCF will have to play nice with ASP.Net. The next thing you need to do is, in each of your service implementations, you need to define the following attribute at the class level.

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

This lets WCF know that you are indeed alright with your service code running under ASP.Net compatibility mode. Now whenever your end up using a protected resource (like open a database connection that is set up to using integrated identity), it will use the identity specified in the “<identity impersonate="true" userName="domain\username" password="xxxxxxx" />” node.

I dont like this solution, because you are opening up WCF to ASP.Net, when in reality WCF is a technology that doesnt care about ASP.Net. So its a shame to make the 2 speak to each other if the only reason we are enabling it is so that we can use the settings in the identity node of the system.web node.

But some times you have got to do what you got to do and in those cases, the above might be a reasonable solution.

3. Use the LogonApi to set the “WindowsImpersonationContext” for the operation that needs to use a different identity to access the resource.

This is a nice solution too, except for the fact that you need to use PInvoke to access the LogonApi (which sits in the advapi32.dll). The reason this is nice is that if you need to use multiple identities for the different resources that you need to access, then the only way to do that is using this method. (Another bad part to this option is that you need to store the username password somewhere, so that you can use it to perform the logon – so be sure to think about encrypting this information).

The code needed is the following:

using System;
using System.Collections.Generic;
using System.Linq;

using System.Runtime.InteropServices;
using System.Security.Principal;

/// <summary>
/// Summary description for LogonAPI
/// </summary>
public class LogonAPI
{
    public const int LOGON32_LOGON_INTERACTIVE = 2;
    public const int LOGON32_PROVIDER_DEFAULT = 0;
    [DllImport("advapi32.dll")]
    public static extern int LogonUserA(String lpszUserName,
        String lpszDomain,
        String lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);
    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int DuplicateToken(IntPtr hToken,
        int impersonationLevel,
        ref IntPtr hNewToken);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool RevertToSelf();

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    public static extern bool CloseHandle(IntPtr handle);


    public static bool impersonateValidUser(String userName, String domain, String password, ref WindowsImpersonationContext impersonationContext)
    {
        WindowsIdentity tempWindowsIdentity;
        IntPtr token = IntPtr.Zero;
        IntPtr tokenDuplicate = IntPtr.Zero;

        if (RevertToSelf())
        {
            if (LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE,
                LOGON32_PROVIDER_DEFAULT, ref token) != 0)
            {
                if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)
                {
                    tempWindowsIdentity = new WindowsIdentity(tokenDuplicate);
                    impersonationContext = tempWindowsIdentity.Impersonate();
                    if (impersonationContext != null)
                    {
                        CloseHandle(token);
                        CloseHandle(tokenDuplicate);
                        return true;
                    }
                }
            }
        }
        if (token != IntPtr.Zero)
            CloseHandle(token);
        if (tokenDuplicate != IntPtr.Zero)
            CloseHandle(tokenDuplicate);
        return false;
    }
    public static void undoImpersonation(WindowsImpersonationContext impersonationContext)
    {
        impersonationContext.Undo();
    }
}

And to use it

WindowsImpersonationContext impersonationContext = null;
if (LogonAPI.impersonateValidUser("username", "domain", "password", ref impersonationContext))
{
    //do work here
    LogonAPI.undoImpersonation(impersonationContext);
}
else
{
   //impersonation failed
}

Are there any other ways to do the above? Is there a better way to do it? (preferably through configuration?)

Design recommendation on using impersonation for multi tiered applications

As a common design recommendation, the further from the client, the less significant its identity should be. In a layered architecture, each layer should run underneath its own identity, authenticate its direct callers, and implicitly trust its calling layer to authenticate its original callers.

See “Trusted Subsystem Pattern” (Patterns & Practices)

The Web service acts as a trusted subsystem to access additional resources. It uses its own credentials instead of the user's credentials to access the resource. The Web service must perform appropriate authentication and authorization of all requests that enter the subsystem. Remote resources should also be able to verify that the midstream caller is a trusted subsystem and not an upstream user of the application that is trying to bypass access to the trusted subsystem.

Aa480587.ch4_trustsub_f01(en-us,MSDN.10).gif

See also:

http://blogs.msdn.com/securitytools/archive/2009/12/30/wcf-security-impersonation.aspx

Monday, April 05, 2010

Connecting to SSRS Web-Service from Visual Studio 2008 (part II)

In a previous post (Connecting to SSRS Web-Services from Visual Studio 2008), I had written about how to connect to SSRS from Visual Studio 2008. In that one I told you how to add a reference to the SSRS web-service using the “Add Web-Reference” option. That is basically a cop-out and even though an easy way of doing it, I wanted to figure out how to do it using VS2008 and WCF.

So basically I wanted to use a WCF client and connect to a ASMX web-service. (So this will work for any project that needs a similar configuration and needs to use the current user credentials for accessing the WCF client).

First add a reference to the SSRS service using the “Add Service Reference” option.

Now use the following code:

using (ReportingService2005SoapClient proxy = new ReportingService2005SoapClient("ReportingService2005Soap"))
{
    //tell WCF that you need the server to impersonate the current user on the server
    //an important piece to get this working is in the config file - where the security is setup
    //the security needs to be setup like so:
    /*
        <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Ntlm" proxyCredentialType="None"
                realm="" />
            <message clientCredentialType="UserName" algorithmSuite="Default" />
        </security>
    */

    proxy.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;

    Property name = new Property();
    name.Name = "Name";

    Property description = new Property();
    description.Name = "Description";

    Property[] properties = new Property[2];
    properties[0] = name;
    properties[1] = description;

    try
    {
        Property[] returnProperties;
        
        //this method's signature is different from what you get if you were to use
        //Add Web Reference. The return value is now an out parameter.
        proxy.GetProperties(new ItemNamespaceHeader(), 
            "/reportfolder/reportname", properties, out returnProperties);

        foreach (Property p in returnProperties)
        {
            Console.WriteLine(p.Name + ": " + p.Value);
        }
    }

    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }


}

I have added comments regarding the important parts of the code.

Now the last thing you need to do is to update your config file so that your security node (part of the Binding element) looks like this:

<security mode="TransportCredentialOnly">
    <transport clientCredentialType="Ntlm" proxyCredentialType="None"
        realm="" />
    <message clientCredentialType="UserName" algorithmSuite="Default" />
</security>

Connecting to SSRS Web-Services from Visual Studio 2008

Or “How to add a web-service reference to a WinForm or a Console based application”.

Its easy to connect to the SSRS web-service from VS2005. Simply add a web-reference by right clicking on the references node and they enter the web-service end-point. (This is typically “http://serverName/ReportServer/ReportService2005.asmx” – note: the 2005 in the end is used even if you are attempting to connect to SSRS2008).

If you use the following code, it should all build correctly and also run.

SSRS.ReportingService2005 rs = new ReportingService2005();
rs.Credentials = System.Net.CredentialCache.DefaultCredentials;

Property name = new Property();
name.Name = "Name";

Property description = new Property();
description.Name = "Description";

Property[] properties = new Property[2];
properties[0] = name;
properties[1] = description;

try
{
    Property[] returnProperties = rs.GetProperties("/Reports/reportName", properties);

    foreach (Property p in returnProperties)
    {
        Console.WriteLine(p.Name + ": " + p.Value);
    }
}

catch (Exception e)
{
    Console.WriteLine(e.Message);
}

If you try to add a webservice reference is VS2008, all you get is an option to add a service reference and if you use the next dialog to connect to your SSRS webservice end-point, the sample code shown above will not compile. (you will get errors regarding no Credentials property available as well as a whole litany of others).
image

Instead here is what you need to do:

Click on the Add Service Reference Option.

Click on the Advanced Button.

The next dialog has the “Add Web Reference” button.

Enter the end-point address in the next dialog.

Now the code shown above should work.

So what is the difference between “Add Web Reference” and “Add Service Reference”? Turns out that you can use the Add Web Reference option only when you have a WSDL document. But if you need to use any WCF based service – not just WSDL based ones, you need to use the “Add Service Reference” option.

Saturday, April 03, 2010

ASP.Net – Editing items in a nested DataList

I had a complex business entity that was setup with a hierarchical relationship (i.e. the parent object contained a list of child objects). I wanted to find a way to display the data such that users could edit items in the contained list.

What I found was the DataList makes it easy to display hierarchically related data using the concept of nested data-lists (I am sure one will be able to use my technique to create any kind of nested databound table). There are quite a few examples of how create nested data-lists and data-grids, but what almost all the samples lacked was the how regarding the editing of the nested data. (the few that I found – just did not work).

First take a look at the implemented code: http://www.aggregatedintelligence.com/NestedDataList/CategoriesView.aspx

Here is how I got it to work:

First the sample data-objects:

image

Category is the parent object and it contains a list of Items.

The goal is to be able to display the data in the following format:

Category    
Name    
Description    
Item-Line1 Item-Line2 Amount
Item-Line1 Item-Line2 Amount
Category    
Name    
Description    
Item-Line1 Item-Line2 Amount
Item-Line1 Item-Line2 Amount

The first step is to drag a DataList onto the web-page. You then add an item template to display the fields from the parent object (Category in this case).

<ItemTemplate>
<div class="divCategoryInfo">
<div>
Category:
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("CategoryName")%>'></asp:Label>
</div>
<div>
Description:
<asp:Label ID="lblLastName" runat="server" Text='<%# Bind("Description") %>'></asp:Label>
</div>
</div>
<div class="divCategoryItems">
<!-- insert items here -->
</div>
</ItemTemplate>

Notice the place holder for the category items, we will come back to that later.

For now, lets plumb up the DataList. In the code-behind, we will perform data-binding during the Page Load event. The databinding needs to be performed ONLY during direct page loads and not during a post-back.

Now for the nested DataList. We could insert a new DataList directly into the ItemTemplate shown above. While that option does work for displaying of the data, it does not allow the nested DataList to be edited. (What used to happen was the ItemEditing event used to fire, but the ItemUpdated never ever fired – which meant that I could never get my hands at the updated data values).

Instead, what I found was that if I put the DataList into a user-control and then dropped it into the parent data-list, it allowed all the events to fire properly. One complication with this method was how to provide the nested data to the inner DataList. Another complication was: during post-back, as data-binding does not occur automatically, how do we again get the correct inner data and manually databind it to the nested data-list?

So here are the basic parts that need to be setup on the DataList that is in the user-control:

First create the user-control.
Next drag a data-list on the user-control. Setup the datalist to display the data.

<asp:DataList ID="dlItems" runat="server" Width="100%" 
OnItemCommand="mySubListItemHandler"
onEditCommand="myListEditHandler"
onUpdateCommand="myListUpdateHandler"
onCancelCommand="myListCancelHandler" BorderStyle="Solid"
BorderWidth="1px" GridLines="Both" >
<HeaderTemplate>
<th></th><th>Line 1</th><th>Line 2</th><th>Amount</th><th>In Shopping Cart</th>
</HeaderTemplate>
<ItemTemplate>
<td>
<asp:LinkButton ID="Linkbutton3" runat="server" CommandName="AddToCart" Text='<%#GetCartTitle(Eval("InShoppingCart")) %>' />
<asp:LinkButton ID="Linkbutton1" runat="server" CommandName="Edit" Text="Edit" />
</td>
<td>
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("Line1")%>'></asp:Label>
</td>
<td>
<asp:Label ID="Label1" runat="server" Text='<%#Bind("Line2")%>'></asp:Label>
</td>
<td align="right">
<asp:Label ID="Label2" runat="server" Text='<%#Bind("Amount")%>'></asp:Label>
</td>
<td align="center">
<asp:ImageButton ID="ImageButton1" runat="server" CommandName="AddToCart"
ImageUrl='<%#GetImageUrl(Eval("InShoppingCart")) %>' />
</td>
</ItemTemplate>
<EditItemTemplate>
<td>
<asp:LinkButton ID="Linkbutton1" runat="server" CommandName="Update" Text="Update" />
<asp:LinkButton ID="Linkbutton2" runat="server" CommandName="Cancel" Text="Cancel" />
</td>
<td>
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("Line1")%>'></asp:Label>
</td>
<td>
<asp:Label ID="Label1" runat="server" Text='<%#Bind("Line2")%>'></asp:Label>
</td>
<td>
<asp:TextBox ID="txtAmount" runat="server" Text='<%#Bind("Amount")%>'></asp:TextBox>
</td>
<td align="center">
<asp:ImageButton ID="ImageButton1" runat="server" CommandName="AddToCart"
ImageUrl='<%#GetImageUrl(Eval("InShoppingCart")) %>' />
</td>
</EditItemTemplate>
</asp:DataList>

The first thing to realize is that I have implemented the handlers for OnItemCommand, OnEditCommand, OnUpdateCommand, OnCancelCommand. The OnItemCommand is strictly not needed, but I wanted to keep track of when a subitem had been clicked. The next thing to realize is that I have an edit template defined for this DataList. It has only one element setup to be edited. (the Amount column).

Now the code-behind. What I realized was that you can get the parent DataList’s item index using the following code:
((System.Web.UI.WebControls.DataListItem)(this.Parent)).ItemIndex;
Now all I needed to do is that when the main page got loaded, I had to store the data in the SessionState, this would give me access to the data in the nested user-control. In my case I stored in list of categories in “Session["list"]”. This meant that every time I needed to perform data-binding in the nested user-control, I had to get the ((System.Web.UI.WebControls.DataListItem)(this.Parent)).ItemIndex item from the Session[“list”] object.
As for when to data-bind: You need to databind in page-load event only if it is not a post-back event. Other times you need to data-bind are when any of the events on the data-list fire (onItemCommand, OnEditCommand, etc).

A little bit about the sample code. It is a sample. I created it quickly to make sure that I could do what I wanted it to do. So dont complain that I dont check for this and I dont check for that. This is not production quality code. The page_load of the categoriesview page is weird because I am using a single data-generation method. It is meant to mock data coming from a different page where I would be selecting the categories that I am interested in. Also, I added some extra handling to keep track of a shopping cart into which items were being added and removed. The items are loaded using a DataProvider which creates the data randomly. The page also uses the MS Ajax framework to make the experience of working with the cart smoother.

Some other things to note. The user-control has an event that the main page subscribes to, to get notified about when an item is added or removed to the cart. Also, the check-box that represent that cart state of an item are simple image-links. The url is dynamically changed during databinding.

You can download the sample code from: http://cid-fbe9049ba8229d5b.skydrive.live.com/self.aspx/Public/WebApp%2004-03-2010.zip

Quotes by the Dalai Lama

 

  1. All major religious traditions carry basically the same message, that is love, compassion and forgiveness the important thing is they should be part of our daily lives.
  2. Happiness is not something ready made. It comes from your own actions.
  3. If you have a particular faith or religion, that is good. But you can survive without it.
  4. If you want others to be happy, practice compassion. If you want to be happy, practice compassion.
  5. In the practice of tolerance, one's enemy is the best teacher.
  6. Love and compassion are necessities, not luxuries. Without them humanity cannot survive.
  7. My religion is very simple. My religion is kindness.
  8. Old friends pass away, new friends appear. It is just like the days. An old day passes, a new day arrives. The important thing is to make it meaningful: a meaningful friend - or a meaningful day.
  9. Our prime purpose in this life is to help others. And if you can't help them, at least don't hurt them.
  10. Sleep is the best meditation.
  11. Sometimes one creates a dynamic impression by saying something, and sometimes one creates as significant an impression by remaining silent.
  12. The ultimate authority must always rest with the individual's own reason and critical analysis.
  13. There is no need for temples, no need for complicated philosophies. My brain and my heart are my temples; my philosophy is kindness.
  14. This is my simple religion. There is no need for temples; no need for complicated philosophy. Our own brain, our own heart is our temple; the philosophy is kindness.
  15. Today, more than ever before, life must be characterized by a sense of Universal responsibility, not only nation to nation and human to human, but also human to other forms of life.
  16. We can live without religion and meditation, but we cannot survive without human affection.
  17. We can never obtain peace in the outer world until we make peace with ourselves.
  18. Where ignorance is our master, there is no possibility of real peace.
  19. Whether one believes in a religion or not, and whether one believes in rebirth or not, there isn't anyone who doesn't appreciate kindness and compassion.
  20. With realization of one's own potential and self-confidence in one's ability, one can build a better world.

C# – Generate a random string

Generating a random string is useful for unit tests (filling your objects with random garbage). Here is a simple snippet to generate random strings/number/symbols in any combination. Useful for suggesting passwords.

static Random random = new Random(DateTime.Now.Second);
private static string PASSWORD_CHARS_LCASE = "abcdefghijklmnopqrstuvwxyz";
private static string PASSWORD_CHARS_NUMERIC = "0123456789";
private static string PASSWORD_CHARS_SPECIAL = "*$-+?_&=!%{}/";
public static string RandomString(int size, bool allowNumbers, bool allowUpperCase, bool allowLowerCase, bool allowSpecialChars)
{
StringBuilder builder = new StringBuilder();

List<char> validCharList = new List<char>();
if (allowNumbers)
validCharList.AddRange(PASSWORD_CHARS_NUMERIC.ToCharArray());
if (allowLowerCase)
validCharList.AddRange(PASSWORD_CHARS_LCASE.ToCharArray());
if (allowUpperCase)
validCharList.AddRange(PASSWORD_CHARS_LCASE.ToUpper().ToCharArray());
if (allowSpecialChars)
validCharList.AddRange(PASSWORD_CHARS_SPECIAL.ToCharArray());

while (builder.Length < size)
{
builder.Append(validCharList[random.Next(0, validCharList.Count)]);
}

return builder.ToString();
}