navigation
 Thursday, July 17, 2008
.NET 3.5 includes new abilities to easily leverage web services and Windows Communication Foundation (WCF) services from AJAX enabled web applications.
posted on July 17, 2008  #    by Noel Rice  Comments [2]
 Wednesday, July 16, 2008
By the time I found the handy JavaScriptSerializer class, it had been introduced, deprecated and brought back from the hellish purgatory where Microsoft keeps objects that have outlived their usefulness.
 |  | 
posted on July 16, 2008  #    by Noel Rice  Comments [0]
 Monday, April 21, 2008
You can't run MSI or CAB files "As Administrator" directly from the explorer in Vista, but there's a workaround.
posted on April 21, 2008  #    by Noel Rice  Comments [0]
 Monday, August 27, 2007
If you need to share resources across multiple pages you can use global resources as opposed to the local resources shown in the previous blog. Global resources are implemented using explicit binding, i.e. server tags like this one:
posted on August 27, 2007  #    by Noel Rice  Comments [0]
 Monday, August 20, 2007

ASP.NET 2.0 has a rich set of localization features built-in. Early in the ASP.NET 1x lifecycle we "rolled our own" using an HTTP handler that reflected for a [localize] tag, looked up the control name in a resource file and assigned the localized value.  I prefer out-of-the-box solutions though when available and the MS resource provider model approach provides enough flexibility to be worthwhile.

ASP.NET 2.0 Localization adds two new resource flavors: local and global resources.  Local resources are used for controls on a specific page. The resources are located in the ASP.NET App_LocalResources folder with the same name as the page you're localizing.  So, if you're translating default.aspx then your App_LocalResources might also contain default.fr-FR.resx with a French translation. Global resources are contained in App_GlobalResource and can be used anywhere in the application.

You bind resources to your controls using explicit or implicit expressions. Explicit expressions use an inline server syntax similar to the data binding syntax you're already familiar with.  Implicit expressions syntax use a "<meta>" tag in the control you're localizing to identify the resource.

Local Resources, Implicit Expressions

It all makes more sense in practice so here's a basic walk-though using local resources with implicit expressions:

  • Create an ASP.NET web application.
  • Add a TextBox control to the default form.
  • While you're on the page to localize (default.aspx in this case), select Tools |Generate local resources. This step has a number of effects. 
    • An App_LocalResources folder appears populated with "Default.aspx.resx". 
    • In the Source view, the ASP.NET markup page tag will have a Culture="auto" attribute added. The value of "auto" lets the application react to the browser language settings.
    • In the ASP.NET HTML markup for the control will have a "meta:resourcekey" attribute added:

      <asp:TextBoxID="TextBox1" runat="server" meta:resourcekey="TextBox1Resource1"></asp:TextBox>

      This identifies a single element in the ASP.NET HTML markup.  If there are other nested elements, each needs to be marked with a "meta:resourcekey" attribute before you can implicitly bind to them (as when you have BoundField elements within a GridView for example).  The general rule is, if the element is qualified with a namespace, it needs a "meta:resourcekey" attribute.

    • The properties are marked with an icon that show they are implicitly bound.

image

  • Double-click default.aspx.resx to edit the resource. This will contain resources for the page as a whole and for any controls on the page. The naming convention for resources is <resourcekey>:property name (you typically don't have to know that, but if localization support for a component is incomplete you can still add the "meta:resourcekey" tags by hand). Add text for the page title and TextBox Text property.

Resource for Default.aspx

  • Copy default.aspx.resx to default.aspx.fr-FR.resx.
  • Edit the values in the resource file to French translations.  You can use a tool like Google Translate to get a fair approximation of what the translation should look like. I wouldn't use it as a production tool though unless you want chuckles from the target audience (try translating to and from the target language a few times and you'll see what I mean). 

image

  • Run the application.  Assuming Internet Explorer as your browser, go to Tools | Options | Languages.  Select the "fr-FR" culture code, then refresh the page and Voila! (or "Blick dort!" or "sguardo là!".  Hmm, sure hope I'm not insulting anyone).

image

Slightly More Complex Scenarios

The same pattern works for more complex objects, such as GridView.  Notice there is a "meta:resourcekey" for the grid and also for each of the BoundField objects.

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataSourceID="SqlDataSource1" meta:resourcekey="GridView1Resource1"> <Columns> <asp:BoundField DataField="ProductName" HeaderText="ProductName" meta:resourcekey="BoundFieldResource1" SortExpression="ProductName" /> <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" meta:resourcekey="BoundFieldResource2" SortExpression="UnitsInStock" /> </Columns> </asp:GridView>

Again, you can bind to any resourcekey/property name combination:

image

And because the page Culture is set to "Auto", you can change the browser language settings and the translated text is displayed automatically:

image

 

The localization facilities built-in to ASP.NET 2.0 should save you some time if they fit your requirements.  If you need a heavier weight solution that uses a database (or other data store) instead of XML to store resources, check out this article by Jeff Modzel "ASP.NET 2.0 Custom SQL ResourceProvider".

posted on August 20, 2007  #    by Noel Rice  Comments [1]
 Tuesday, July 17, 2007

There is a pattern to these errors.  Same asp.net source, same database, same IIS version, same Visual Studio version, same components version, but still some chunk of JavaScript is absent without leave. The actual error messages tend to be new each time, but at least the pattern is recognizable. The reason is that the missing JavaScript functions live in webresource.axd, webresource.axd is handled by an aspnet_isapi.dll, and chances are you are missing a mapping for "axd" in your IIS configuration for your web site. To fix:

  • In the IIS Microsoft Management Console snap-in, right-click the virtual directory and select Properties.
  • On the Virtual Directory tab click the Configuration button. 
  • On the Mappings tab of the Application Configuration dialog click the Add button. This brings you to the Add/Edit Application Extension Mapping dialog. Enter for the Executable the full path to aspnet_isapi.dll located in .NET framework directory.  In this case the path was C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll. Set the Extension to be ".axd".  Don't forget the dot or the dialog will not let you proceed. Uncheck the "Check that file exists" checkbox.
  • If the OK button doesn't become enabled, there's a weirdity here (in the tech support business we used to call these "issues", in the real world we call it a "bug").  Click the Executable textbox entry and the OK button should become enabled.

Cycle the website and try again.  Different flavors of this problem occur periodically, so tag this page for later and save yourself some pain.

posted on July 17, 2007  #    by Noel Rice  Comments [0]
 Saturday, July 14, 2007

You may run into the 15023 error if you restore a MS SQL database from backup.  You expect a restored database to be in exactly the same state as the backup, but the login fails for a user that had permissions in the backed up database.  When you use the "User Mapping" SQL Management Studio functionality to allow the user permissions to the new database, you receive the 15023 error.  This is caused by Security identification numbers (SID) that are mismatched or 'orphaned' in the sysusers table. 

The SQL Server stored proc sp_change_users_login locates and fixes these records.  Run it with a single parameter 'Report' to get a listing of abandoned user names and corresponding SIDs:

exec sp_change_users_login Report

The 'Update_One' parameter will reconnect a single login:

exec sp_change_users_login Update_One, 'MyLogin', 'MyLogin'

You can find more info about this issue at:

http://support.microsoft.com/kb/246133

http://support.microsoft.com/kb/240872

This next blog expands on the available parameters for sp_change_users_login:

http://blog.sqlauthority.com/2007/02/15/sql-server-fix-error-15023-user-already-exists-in-current-database/

Also, try checking out the source for sp_change_users_login found in the Sql Server Management Studio under Databases | System Databases | Master | Programmability | Stored Procedures | sp_change_users_login.

posted on July 14, 2007  #    by Noel Rice  Comments [0]
 Sunday, July 08, 2007

SQL Management Objects (SMO) is something of a Swiss army knife that lets you traverse meta data, automate backup and restore, and otherwise manage SQL Server 2005 through .NET code.  For instance, to perform the classic database hierarchy walk use the SmoApplication object and enumerate the servers.  First you need to reference the Smo assemblies:

Microsoft.SqlServer.ConnectionInfo.dll
Microsoft.SqlServer.Smo.dll
Microsoft.SqlServer.SmoEnum.dll

Then call the SmoApplication EnumAvailableSqlServers() method, passing 'true' to list only local servers:

1
DataTable tblServers = SmoApplication.EnumAvailableSqlServers(true);

EnumAvailableSqlServers() may not work if you don't have a network connection, but you can use the RegisteredServers collection property instead. This will work without network connection and will pick up server instances for SQL 2000/2005, SQL Express and MSDE. Here the heirarchy follows the expected pattern of Server/Database/Table:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
foreach (RegisteredServer registeredServer in SmoApplication.SqlServerRegistrations.RegisteredServers)
{
Server server = new Server(registeredServer.Name);
Console.WriteLine("Server: {0} Version: {1}",
registeredServer.Name, server.PingSqlServerVersion(server.Name).Major);

foreach (Database database in server.Databases)
{
if ((!database.IsSystemObject) && (database.IsAccessible))
{
Console.WriteLine("Database: {0}", database.Name);

foreach (Table table in database.Tables)
{
Console.WriteLine("Table: {0} Rows: {1}", table.Name, table.RowCount);
}
}
}
}

There are a large number of collections and enumerating methods, for example: stored procedures, user defined types, roles, rules, schemas, locks and permissions.  SMO works against Sql Server earlier than 2005, but some methods may not be supported.  You can handle that by checking the sql version:

1
2
3
4
5
6
7
if (server.PingSqlServerVersion(server.Name).Major > 8)
{
foreach (UserDefinedDataType udf in database.UserDefinedDataTypes)
{
Console.WriteLine("Type: {0}", udf.Name);
}
}

A nifty SMO bonus is that objects Database and downwards in the hiearchy have a Script StringCollection property.  Script is pre-populated with T-SQL and can be used to recreate objects.

SMO provides DBA automation functionality like backup/restore and create/drop databases. These operations depend on the SMO Server object.  The constructor for Server can have a) no parameters for your local server, b) a server name only, or c) a ServerConnection object in case you need to supply user name and password.

1
2
ServerConnection serverConnection = new ServerConnection("MyMachine", "sa", "MyM@ch1n3");
Server raServer = new Server(serverConnection);

Here is an example of creating a new database to be used as a backup location:

1
2
3
4
5
6
7
8
static Database CreateBackupDatabase(Server server, string databaseName)
{
string backupDatabaseName = databaseName + "_" + DateTime.Now.ToString("yyMMddHHmmffffff");
Database backupDatabase = new Database(server, backupDatabaseName);
Console.WriteLine("Creating new database {0}", backupDatabase.Name);
backupDatabase.Create();
return backupDatabase;
}

The backup operation requires a backup "device", in this case a file that will be used to store the backup.  Assign an SMO Database object to be backed up, add the backup device to its list of devices and call the SqlBackup() method.  You may also want to assign event handlers for PercentComplete and Complete events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void DoBackup(Server server, string databaseName, Database backupDatabase)
{
Console.WriteLine("Backing up {0}", databaseName);

BackupDeviceItem backupDevice =
new BackupDeviceItem(databaseName + ".bak", DeviceType.File);

Backup backup = new Backup();
backup.Database = databaseName;
backup.Devices.Add(backupDevice);
backup.PercentComplete += new PercentCompleteEventHandler(InProgress);
backup.Complete += new ServerMessageEventHandler(Complete);
backup.SqlBackup(server);
}

static void InProgress(object sender, PercentCompleteEventArgs e)
{
Console.WriteLine("Percent complete: {0}", e.Percent);
}

Restoring can sometimes be trickier due to the original database having a strangle-hold on the physical data and log files.  To work around this use the Restore RelocateFiles property to map new file names and locations. Also notice that to get the physical file path we use the Database object's FileGroups property, drill down into the Files and use the path of the first file in the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void DoRestore(Server server, string databaseName, Database backupDatabase)
{
Console.WriteLine("Restoring {0}", backupDatabase.Name);
Restore restore = new Restore();
restore.Database = backupDatabase.Name;
restore.ReplaceDatabase = true;
string backupPath = Path.GetDirectoryName(backupDatabase.FileGroups[0].Files[0].FileName);
string dataPath = string.Format("{0}\\{1}.mdf", backupPath, backupDatabase.Name);
restore.RelocateFiles.Add(new RelocateFile("Falafel", dataPath));
restore.RelocateFiles.Add(new RelocateFile("Falafel_log", Path.ChangeExtension(dataPath, ".ldf")));
BackupDeviceItem backupDevice =
new BackupDeviceItem(databaseName + ".bak", DeviceType.File);
restore.Devices.Add(backupDevice);
restore.PercentComplete += new PercentCompleteEventHandler(InProgress);
restore.Complete += new ServerMessageEventHandler(Complete);
restore.SqlRestore(server);
}

Thats a few of the things SMO can do for you. It's not a replacement for T-SQL, but if you need access to meta-data, automation or other DBA tasks from managed code, then the SMO namespace may be worth exploring. BTW, you can also use SMO in PowerShell directly, wrapped in commands or as the infrastructure for PowerShell providers.

posted on July 8, 2007  #    by Noel Rice  Comments [2]
 Thursday, June 21, 2007

You can write your own cmdlet ("command-let") to extend PowerShell in the .NET language of your choice.  You need to write both the cmdlet and a PowerShell snap-in to help install and register the command.  Here's an example snap-in for a "get-food" command  that lists tasty Mediterranean foods (like Falafels):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.Collections.ObjectModel; // supports Collection
using System.Management.Automation; // supports PSSnapIn
using System.Management.Automation.Runspaces; // supports *ConfigurationEntry
using System.ComponentModel; // supports RunInstaller
using Falafel;

// Project also references System.Configuration.Install

public class FalafelSnapIn : CustomPSSnapIn
{
public FalafelSnapIn()
: base()
{
}

public override string Name
{
get
{
return "FalafelSnapIn";
}
}

public override string Vendor
{
get
{
return "Falafel Software ";
}
}

public override string Description
{
get
{
return "Runs custom Falafel commands.";
}
}

/// <summary>
/// Specify the cmdlets that belong to this custom PowerShell snap-in.
/// </summary>
private Collection<CmdletConfigurationEntry> _cmdlets;
public override Collection<CmdletConfigurationEntry> Cmdlets
{
get
{
if (_cmdlets == null)
{
_cmdlets = new Collection<CmdletConfigurationEntry>();
_cmdlets.Add(
new CmdletConfigurationEntry("get-food", typeof(FalafelCmdlet), null));
}

return _cmdlets;
}
}
}

CustomPSSnapIn knows how to be installed via installutil.exe, contains information about name, vendor, description etc., and has collections of types that can be registered with PowerShell such as cmdlets, Types, Formats and Providers.  FalafelSnapIn descends from CustomPSSnapIn, overrides the Cmdlets collection and adds the "get-food" cmdlet to the collection. Notice that much of the PowerShell specific functionality is supported in the System.Management.Automation namespace.

 Next we'll look at FalafelCmdlet, the implementing class for the "get-food" cmdlet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Management.Automation; // supports PSSnapIn, CmdLet, Parameter

namespace Falafel
{
[Cmdlet(VerbsCommon.Get, "Food")]
public class FalafelCmdlet : Cmdlet
{
private string _contains;

[Parameter(Mandatory = false, Position = 0, HelpMessage =
"List item descriptions containing this string")]
public string Contains
{
get { return _contains; }
set { _contains = value; }
}

protected override void ProcessRecord()
{
MediterraneanFoods foods = new MediterraneanFoods();
foreach (MediterraneanFood food in foods.FindFoods(_contains))
{
WriteObject(food);
}
}
}
}

First the Cmdlet attribute marks this class as a cmdlet and helps formalize the naming convention for cmdlets as being verb-noun combinations.  VerbsCommon lists the verbs that may be used:

FalafelCmdlet descends from Cmdlet but you can also use PSCmdlet.  Cmdlet is lighter-weight but PSCmdlet has more access to the PowerShell runtime. For this example the functionality would be the same so I will go with the lighter-weight Cmdlet.  The Contains property in this example holds a string used in searching food descriptions.  The Parameter attribute marks the Contains property as a parameter for the cmdlet, provides a help string and identifies Contains as not being mandatory.

Finally the ProcessRecord() method of Cmdlet is overridden to perform the actually work of the command.  In ProcessRecord() a class called MediterraneanFoods returns a generic list of MediterraneanFood objects based on description.  We won't get into the workings of MediterraneanFoods here because its purpose is to simply provide sample functionality for the command.  Note: Watch this space for a tasty blog by Lino on Anonymous Delegates that gets into how the generic list is searched.

The really cool part of ProcessRecord() is the WriteObject() method of Cmdlet.  Instead of Console.Writeline() text-only output, WriteObject() actually ouputs MediteraneanFood objects into the PowerShell pipeline.  This means that your objects are automatically usable by other commands.  You'll see this in a minute when we register and run the command. 

Here are the PowerShell commands I use to install and register the cmdlet:

cd C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug
set-alias iu $Env:windir\Microsoft.NET\Framework\v2.0.50727\installutil.exe
iu FalafelCmdletLibrary.dll
Add-PSSnapin FalafelSnapIn

The first line changes the directory to where the assembly for the FalafelCmdlet is stored.  Then, as a convenience you can use set-alias to make access to InstallUtil.exe easier.  The "IU" alias for InstallUtil installs the assembly.  This step produces a certain amount of logging I won't include here.  Finally the Add-PSSnapin makes the snap-in available to the current PowerShell console session.  You can call Get-PSSnapin to see the description and verify it's there:

Now we can run the "get-food" command, passing the "contains" parameter.  Notice the output by default is in table format.

Remember the call to WriteObject() that makes all our output actual objects instead of text?  Here's an example of piping the output of the one command that will easily work with an existing command without any adapting or parsing necessary to make it all work.  The "|" character is used to pipe the output from get-food to a format-list:

...and we have the output in list, not table, format.  Or we could pipe the output to the get-member command that performs reflection on objects passed to it.  You can see the MediterraneanFood object has Description and Name properties:

This all opens up possibilities for you to wrap existing .NET functionality in command-line form and to use the output in other existing commands.

posted on June 21, 2007  #    by Noel Rice  Comments [1]
 Tuesday, June 12, 2007

At Tech Ed in Orlando last week John Waters picked up "Windows PowerShell Unleashed by Tyson Kopczynski" and as John has a eye for cool technology I did likewise and got hooked on PowerShell.  I'm not usually a live-on-the-command-line-love-batch-files kind of guy but PowerShell covers enough ground in a powerful and consistent way that I'm considering adding this to my technology toolbox.  Although PowerShell is intended for system administrator use, there may be a place for PowerShell in development to do limited testing against .NET objects, task automatation and general exploration.  In any case it's a great toy and a hoot to play with.

PowerShell has several important differences from cmd.exe. 

  • PowerShell lets you use NET FCL objects, COM objects and even your own .NET classes.
  • PowerShell scripts can be code signed (see Windows PowerShell Unleashed by Tyson Kopczynski for detailed steps).  Previous scripting environments like Windows Scripting Host (WSH) opened large security holes.  This feature allows scripts from trusted sources to be run.
  • PowerShell is object based, not text based.  This eliminates parsing and reformatting to use the output from a command.
  • PowerShell provides a consistent interface. 
    • You can navigate through files, the certificate store, environmental variables and the registry, all using commands you already know. 
    • Commands confirm to the pattern verb-name, i.e. "get-service" to cut down on memorization.  There are aliases for historic DOS and UNIX commands so you can list a directory with the PowerShell native "get-childitem", UNIX style "ls" or DOS "dir". 
    • PowerShell lets you locate and interrogate available commands and objects.
  • PowerShell is extensible.  You can create your own commands in a .NET assembly and register them for use in PowerShell.  There are several other points of extensibility including providers for navigation, types and formatting.

Here's a sample session of PowerShell to give you a very brief notion of how it works.  Be aware that this is only scratching the surface of the possibilities for PowerShell.  Let's say we want to work with Windows services, so we need to know what commands are available:

 
PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> get-command *service
CommandType     Name                                                Definition
-----------     ----                                                ----------
Cmdlet          Get-Service                                         Get-Service [[-Name] <String[]>] [-Include <Stri...
Cmdlet          New-Service                                         New-Service [-Name] <String> [-BinaryPathName] <...
Cmdlet          Restart-Service                                     Restart-Service [-Name] <String[]> [-Force] [-Pa...
Cmdlet          Resume-Service                                      Resume-Service [-Name] <String[]> [-PassThru] [-...
Cmdlet          Set-Service                                         Set-Service [-Name] <String> [-DisplayName <Stri...
Cmdlet          Start-Service                                       Start-Service [-Name] <String[]> [-PassThru] [-I...
Cmdlet          Stop-Service                                        Stop-Service [-Name] <String[]> [-Force] [-PassT...
Cmdlet          Suspend-Service                                     Suspend-Service [-Name] <String[]> [-PassThru] [...

From here we can see what services are available for SQL Server:

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> get-service MSSQL*
Status   Name               DisplayName
------   ----               -----------
Running  MSSQL$NRLAPTOP2    MSSQL$NRLAPTOP2
Running  MSSQL$SQLEXPRESS   SQL Server (SQLEXPRESS)
Running  MSSQL$TELERIK      MSSQL$TELERIK
Running  MSSQLSERVER        SQL Server (MSSQLSERVER)
Stopped  MSSQLServerADHe... SQL Server Active Directory Helper
Running  MSSQLServerOLAP... SQL Server Analysis Services (MSSQL...

Now we want to stop the MSSQLSERVER service and any dependant services:

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> stop-service "MSSQLSERVER" -force

If we re-run get-service we can see that the service is stopped:

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> get-service mssql*
Status   Name               DisplayName
------   ----               -----------
Running  MSSQL$NRLAPTOP2    MSSQL$NRLAPTOP2
Running  MSSQL$SQLEXPRESS   SQL Server (SQLEXPRESS)
Running  MSSQL$TELERIK      MSSQL$TELERIK
Stopped  MSSQLSERVER        SQL Server (MSSQLSERVER)
Stopped  MSSQLServerADHe... SQL Server Active Directory Helper
Running  MSSQLServerOLAP... SQL Server Analysis Services (MSSQL...

You can also interrogate the service objects using the get-member command. For example you could take the service objects returned by get-service and direct them to the get-member command using the "|" pipe symbol. The following is only a partial listing.

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> get-service | get-member
   TypeName: System.ServiceProcess.ServiceController
Name                      MemberType    Definition
----                      ----------    ----------
Name                      AliasProperty Name = ServiceName
Close                     Method        System.Void Close()
Continue                  Method        System.Void Continue()
CreateObjRef              Method        System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType)
Dispose                   Method        System.Void Dispose()
Equals                    Method        System.Boolean Equals(Object obj)
ExecuteCommand            Method        System.Void ExecuteCommand(Int32 command)
get_DependentServices     Method        System.ServiceProcess.ServiceController[] get_DependentServices()
get_DisplayName           Method        System.String get_DisplayName()
get_MachineName           Method        System.String get_MachineName()
get_ServiceHandle         Method        System.Runtime.InteropServices.SafeHandle get_ServiceHandle()

What about navigation?  If I want to change locations in the file system of course there's "CD" or the PS native "set-location". What's unique here is that you can navigate the registry, environmental variables, certificate stores or any other system that PS has a provider for (yes, you can write your own providers).  For example the following is perfectly legal:

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> cd env:
PS Env:\> dir
Name                           Value
----                           -----
Path                           C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\Program Files\ATI Technolo...
TEMP                           C:\DOCUME~1\NOELRI~1\LOCALS~1\Temp
SESSIONNAME                    RDP-Tcp#1
PATHEXT                        .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.PSC1
USERDOMAIN                     NRLAPTOP
PROCESSOR_ARCHITECTURE         x86

You could image using CD to navigate hiearchical database information in this way (to what practical end I do not know, but it is amusing). Find out what PowerShell drives are available on your machine by using get-psdrive:

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> get-psdrive
Name       Provider      Root                                                                           CurrentLocation
----       --------      ----                                                                           ---------------
Alias      Alias
C          FileSystem    C:\                                    Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug
cert       Certificate   \
D          FileSystem    D:\
Env        Environment
Function   Function
HKCU       Registry      HKEY_CURRENT_USER
HKLM       Registry      HKEY_LOCAL_MACHINE
Variable   Variable

Use get-psprovider to find the providers on your system.  See the MSDN for examples of writing your own provider. 

PS C:\Clients\Falafel\Projects\FalafelCmdletLibrary\bin\Debug> Get-PSProvider
Name                 Capabilities                                      Drives
----                 ------------                                      ------
Alias                ShouldProcess                                     {Alias}
Environment          ShouldProcess                                     {Env}
FileSystem           Filter, ShouldProcess                             {C, D}
Function             ShouldProcess                                     {Function}
Registry             ShouldProcess                                     {HKLM, HKCU}
Variable             ShouldProcess                                     {Variable}
Certificate          ShouldProcess                                     {cert}

You can download PowerShell at http://www.microsoft.com/technet/scriptcenter/topics/msh/download.mspx.  It comes with the install and docs for "Getting Started", "Quick Reference" and "Users Guide".

posted on June 12, 2007  #    by Noel Rice  Comments [1]
 Tuesday, May 15, 2007

There's a fascinating little article "Create Advanced Web Applications With Object-Oriented Techniques" by Ray Djajadinata that delves into how JavaScript implements objects, inheritance, anonymous methods, and even a bit about simulating private properties and namespaces.  This may be relevant if you want greater depth in AJAX, particularly the Microsoft flavor ASP.NET AJAX.  There's a sidebar (by Bertrand Le Roy) about the ASP.NET AJAX OOPS implementation and use of JavaScript to add reflection and other .NET familar constructs including properties, events, enumerations and interfaces.

posted on May 15, 2007  #    by Noel Rice  Comments [0]
 Wednesday, May 02, 2007

We recently completed a self-paced tutorial for Telerik RadControls.

You can download the self paced tutorial from the Telerik site at:

http://www.telerik.com/support/self-paced-tutorial.aspx

The tutorial addresses the entire suite of RadControls, AJAX, client-side scripting, and custom data-binding techniques.

posted on May 2, 2007  #    by Noel Rice  Comments [0]
 Tuesday, May 01, 2007

You can support docking in your web applications using Telerik's new control suite "Prometheus".  Prometheus is completely redesigned to use Microsoft's ASP.NET Ajax.  The Prometheus docking controls make it easy to define objects that may be dragged and areas where objects may be dragged to.  With docking support you can create web portal sites, "PageFlake" style web pages (where the user can dynamically add controls and drag them around on the page), or even "post-it notes" can be added to the page.  The current state-of-play is that the controls are in beta and have a few quirks, but the performance is very responsive and the look and feel is also quite good.

To use Prometheus "RadDock" controls first download and install the Microsoft ASP.NET Ajax extensions at http://ajax.asp.net/, then get the free Prometheus beta download at: http://www.telerik.com/products/aspnet-prometheus/download.aspx.  Once installed, create a new project type "ASP.NET AJAX-Enabled Web Application".  BTW, if you try to use the standard ASP.NET web application you will get really interesting results.

Your toolbox will have the new AJAX extension controls, including "ScriptManager".  ScriptManager is the workhorse of Microsoft's ASP.NET Ajax that registers client side scripts that enable AJAX functionality. The ScriptManager is automatically placed on the default web page so you don't need to do anything further there.

Also in the toolbox are the new Prometheus controls.  The three you need for drag and drop support are:

  • RadDock is the container for text or other controls that need to be dragged on the web page.
  • RadDockZone defines an area on the screen where a RadDock can dropped onto.
  • RadDockLayout can contain a number of RadDockZone controls so you can set the skin for everything at once.  RadDockLayout also has a property StoreLayoutInViewState that can be used to persist the RadDock locations across multiple postbacks.

To test these controls drop a RadDockLayout, two RadDockZones within the RadDockLayout and RadDock controls in each of the RadDockZones. 

How about adding content to the RadDock? 

  • Use the Radock Text property for simple text only that doesn't involve any other controls.
  • Create your own ITemplate implementation class and set the RadDock ContentTemplate property to that class in the code behind.
  • Add to the ContentTemplate property markup.  At this stage of development I don't see a smart tag or other UI assistance so instead add a ContentTemplate tag and add controls within the tag.

For example we could add another new Prometheus control "RadColorPicker":

<ContentTemplate>
  <telerik:RadColorPicker ID="RadColorPicker1" runat="server" Preset="Standard">
  </telerik:RadColorPicker>
</ContentTemplate>

To finish up we set the Skin property of RadDockLayout to "Longhorn".  If you've used the current version of RadDock you're used to adding various skin files to the project.  Not with the Prometheus version where the skins are built-in and you can choose the Skin property value from a drop down list.  Finally you can set the title bar text for each RadDock using the Title property.

When you run the Prometheus version of the docking controls you should experience snappy responsiveness and a very respectiable UI.  Try downloading Prometheus and retrofit some part of your current web application to support docking.  Enjoy!

Note: if you want to learn Telerik RadControls from the ground up, check out the Falafel-authored, self-paced tutorial at http://www.telerik.com/support/self-paced-tutorial.aspx and download the sample projects from our community download site at http://www.falafel.com/community/files/Default.aspx.

posted on May 1, 2007  #    by Noel Rice  Comments [1]
 Tuesday, April 17, 2007

You might overlook the JavaScript debugging utility that's already built in to Visual Studio 2005: the Script Explorer window.  The Script Explorer can take care of the usual debugging tasks like stepping through code, adding watches and evaluating variables. 

To use the debugger in Internet Explorer navigate to the browser Tools | Internet Options | Advanced tab and make sure that "Disable script debugging" is turned off. 

Run your web application in Visual Studio 2005.  Then select the menu option for Debug | Windows | Script Explorer.  Notice in the background the tags for telerik RadEditor controls...

The first thing you notice in the Script Explorer window is a series of JavaScript and resource files that are currently loaded.  Double click on the aspx file you're currently working with and you will see the evaluated HTML returned from the server.  The RadEditor control now shows as its computed HTML, CSS and JavaScript that will actually be functioning in the browser. 

You can also navigate up to the script for the page and set breakpoints and watches.  When the JavaScript executes and hits your breakpoint you get all the usual Visual Studio debugging capabilities for free.

Next blog I'll show the excellent "Firebug" debugging utility for Firefox.  Firebug doesn't stop at just JavaScript but works with the entire stack of AJAX related technologies (and has a high cool-factor).

posted on April 17, 2007  #    by Noel Rice  Comments [0]

The Firebug debugger add-in for Firefox handles the entire stack of AJAX related technologies.  Ever wanted to tweak the margins in your style sheet while you watch the changes?  Profile a web page and see a visual representation of when scripts are loading and how big they are?  Watch the XmlHttpRequest (i.e. AJAX) requests move over the wire in real time?  You can do all this in Firebug, and of course you can step through your JavaScript code.  At Falafel we use this tool in our consulting work and telerik recommends Firebug for use in web applications using their RadControl suite.

Firebug is an innovative tool that handles usual tasks you would expect from a combined DOM explorer, AJAX/JavaScript profiler, and JavaScript debugger.  But it combines technologies in a new way that is definitely cool and a lot of fun to use.  This will take a few blogs to talk about in depth, but this should get you started.

Firebug only installs and runs in Firefox.  Get Firefox at http://www.mozilla.com/en-US/firefox if you don't already have it installed.  In Firefox download and install Firebug from http://www.getfirebug.com/Now run any page in Firefox and notice the green checkbox in the lower right hand corner.  Click it to start up Firebug for the page you're on. 

The Console tab is used for logging output.  The logging statements can be embedded in your script or run interactively on the Firebug interactive JavaScript command line.  The image below shows the special "dir" command line API dumping the contents of the "<body>" tag to the console.  A series of "console" commands output with visually helpful icons.  There's more on this tab that will wait for another blog (or jump ahead by checking out the API documentation at http://www.getfirebug.com/docs.html).

If you couldn't wait and are running Firebug right now, try clicking the Inspect button, then move your mouse on a web page.  The HTML and Style tabs will display the corresponding markup in real time as you move.  Click once on the page to stop inspecting.  Notice the crossed out items in the Style window?  The window is showing how styles are cascading and what styles are not in effect.  Try double clicking a style value -- you can edit the value and see the results immediately!  Also notice when your mouse cursor passes over a color or image tag that a thumbnail pops up.  Very smooth...

Try clicking on the Edit button (next to the Inspect button).  You can edit the HTML and see the results.  Feel free to add a completely different tag like the image below reading "Modify HTML on-the-fly!!".

By the way, when you click the refresh button, all changes go away.  The author of this tool, Joe Hewitt, mentions in his talk about the advanced features of Firebug (http://yuiblog.com/blog/2007/01/26/video-hewitt-firebug/) that this version of Firebug is not intended to be an editor, but more of an exploring and auditioning tool. 

Speaking of auditioning new settings, what about style layout settings?  The Layout tab shows the offset, margin, border and padding for each element you select.  In the browser you will see rules and other visual metric devices overlaying the web page.  Now try clicking one of the settings, say top padding for an image as shown below.  You don't have to enter a number off the keyboard.  Instead try the arrow keys to raise and lower values.  That way you can keep your eye on the layout until it's just right.

Firebugs profiling features show you the JavaScript and XHR requests going over the wire.  The example below is a demo using a set of telerik date controls using a RadAjaxManager to AJAX enable the whole process (thanks to John Waters for letting me steal the example).  In the Net tab we can select to see all or only certain traffic.  The image below shows all the JavaScript traffic; when it loads and how big each piece is.  For the web resources that contain images you can pass the mouse over to see thumbnails.  Click the plus sign to get the details like HTTP headers, requests and responses.

If we click the XHR tab we see only traffic initiated by the XMLHttpRequest object.  XMLHttpRequest is a major component of AJAX, so this feature is very important for evaluating web site performance with and without AJAX, tweaking AJAX performance, and even checking XHR traffic for security vulnerabilities. 

Firebug is after all a debugger.  All the capabilities you expect like step over, step into, run to line, step out, conditional breakpoints, watches, and automatic local variable display are there.  The conditional breakpoint window is a nice piece of UI programming in itself (see below).

Note: Thanks to Ramesh Theivendran for letting me steal the code for this XMLHttpRequest demo (I see a pattern forming here). 

This has been the briefest look at a tool that is sure to set the bar for all web debuggers.  In coming blogs I'll show Firebug in more depth, but until then I hope you try it yourself.

 |  |  | 
posted on April 17, 2007  #    by Noel Rice  Comments [1]
 Tuesday, March 13, 2007

You may need the public key token for purposes such as registering an HTTP module.  Use the strong naming tool with the -T option to extract the public key.  Be sure to call sn from the command line that comes with the .NET framework SDK:

C:\Program Files\telerik\r.a.d.controlsQ4 2006\NET2\bin>sn -T radupload.net2.dll

Microsoft (R) .NET Framework Strong Name Utility  Version 2.0.50727.42
Copyright (c) Microsoft Corporation.  All rights reserved.

Public key token is b4e93c26a31a21f0

posted on March 13, 2007  #    by Noel Rice  Comments [0]
 Wednesday, February 14, 2007

How do you find if a table is referenced in a stored proc?  That can be quite the Easter egg hunt without some sort of tool.  The following SQL uses the syscomments table (contains the stored proc text) and joins to the sysobjects table to determine which syscomment records belong to stored procs.  CHARINDEX returns the starting position of an expression in a character string, so the fact that it returns greater than zero signals a hit.

DECLARE @SEARCHSTRING VARCHAR(255)

SELECT @SEARCHSTRING = 'MyTable'

SELECT DISTINCT sysobjects.name
FROM sysobjects,syscomments
WHERE sysobjects.id = syscomments.id
-- look for stored procs only
AND sysobjects.type = 'P'
AND sysobjects.category = 0
-- what you are looking for, what you're looking in
AND CHARINDEX(@SEARCHSTRING,syscomments.text)>0

This is a reduced version taken from an example at:

http://wiki.ittoolbox.com/index.php/HOWTO:Search_the_text_of_triggers_and_stored_procedures

Another handy related query from Adam Anderson's toolbox of SQL techniques finds references to column names.  This one lists the table names where tables contain "Created" and "Modified" users and dates:

select distinct so.name
from sysobjects so
where exists (
select *
from syscolumns sc
where sc.id = so.id
and sc.name in ( 'CreatedBy', 'CreatedOn', 'ModifiedBy', 'ModifiedOn' )
)

More info about CHARINDEX at http://msdn2.microsoft.com/en-us/library/ms186323.aspx.

posted on February 14, 2007  #    by Noel Rice  Comments [1]
 Wednesday, February 07, 2007

This morning I discovered I could enter code to VS but couldn't backspace over it.  This would require me to code perfectly the first time, and for me that's a non-starter.  It turns out that none of the command buttons work.  The short story on the fix for this oddity is to either disable Auto-Hide or open one of the hidden windows, then click the X to close.  More info at the msdn blog site: Tabs, Backspace, Delete and other commands not working in the editor.

posted on February 7, 2007  #    by Noel Rice  Comments [0]
 Wednesday, January 24, 2007

.NET 3.0 has a number of command line utilities like the service utility (svcutil.exe) that can be awkward to run if you're already in Explorer, deep in a folder structure.  Phillip's svn blog reminded me of a trick with the registry to get the command prompt window populated with the current path.

30netprompt.gif

You can use this technique for any command line or batch file you want to attach to the Explorer context menu. 

  1. Add a key (any name and content appears to work) to HKEY_CLASSES_ROOT\Folder\shell.
  2. Below that add a key "command" and set the text to be whatever command you want executed.  I copied the command line from the .NET 3.0 SDK "CMD Shell" (see registry export listing below), but you could use any command line entry.  The nice thing about the .NET 3.0 cmd shell is that it sets the environment so you can access svcutil.exe and other 3.0 specific utilities.

Registry export listing:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Folder\shell\Command Prompt]
@=".NET 3.0 Command Prompt"

[HKEY_CLASSES_ROOT\Folder\shell\Command Prompt\command]
@="C:\\WINDOWS\\system32\\cmd.exe /E:ON /V:ON /T:0E /K \"C:\\Program Files\\Microsoft SDKs\\Windows\\v6.0\\Bin\\SetEnv.Cmd\""

Built in to Windows Vista

If you're running Vista then you're in luck, it's built-in!  Check out this article showing how to shift-right-click a folder to get the "Command Prompt Here" context menu item:

http://blogs.msdn.com/tims/archive/2006/09/18/windows-vista-secret-1-open-command-prompt-here.aspx

posted on January 24, 2007  #    by Noel Rice  Comments [1]
 Tuesday, January 23, 2007

While it's true that the .NET 3.0 doesn't directly add any ASP.NET functionality, it's also true that the .NET 3.0 install makes changes in your \Framework\v2.x directory.  Although the install introduction warned that a reboot might be necessary, it didn't turn out to be necessary and the install was relatively painless.  ...Untill we tried to install on a server running a 2.x webservice and received an error indicating that assembly ServiceModel.DLL couldn't be found. 

The primary assembly for Windows Communication Foundation (WCF) services is ServiceModel.DLL. Another seemingly unrelated fact is that hosting a WCF service in IIS requires a service file with a ".svc" extension in a virtual directory.  How is the svc file processed?  An Http handler for the "*.svc" extension is associated with ServiceModel.DLL.  And how does the .NET 2.0 based ASP.NET know to suddenly start looking for this service file?  The 3.0 install adds a new http handler to the web.config file in the \WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG directory.

<configuration>
  <system.web>
  ...
    <httpHandlers>
    ...
 <add
           path="*.svc"
           verb="*"
           type="System.ServiceModel.Activation.HttpHandler,
           System.ServiceModel, Version=3.0.0.0, ...

An IIS restart allowed the web service to resume without error.

posted on January 23, 2007  #    by Noel Rice  Comments [1]
 Tuesday, November 07, 2006

Serializing objects to XML and back again in C# is trivial until you need greater control over the operation.  How do you serialize binary data, Color properties or some object type that hasn't been invented yet?  Implementing IXmlSerializable allows you to read and write object data in whatever format and by any means you choose. 

The example serializes a list of objects with a single Color property. 

<?xml version="1.0" ?>
<Items>
    <Item>Red</Item>
    <Item>White</Item>
    <Item>Blue</Item>
</Items>

The sample below serializes "MyObjects.xml" from disk to populate a MYObjectList object, makes changes to the list and re-writes "MyObjects.xml".  The two important IXmlSerializable methods to override are ReadXml() and WriteXml().  ReadXml() consumes an XmlReader instance.  Here we use a XmlTextReader to pull the xml text off the disk.  Likewise, WriteXml() uses a XmlTextWriter instance to recreate the xml file.
 
class Program
{
  static void Main(string[] args)
  {
    MyObjectList myObjectList = new MyObjectList();

    XmlTextReader reader = new XmlTextReader("MyObjects.xml");
    myObjectList.ReadXml(reader);

    myObjectList.Items.Clear();
    myObjectList.Items.Add(new MyObject(Color.BlanchedAlmond));
    myObjectList.Items.Add(new MyObject(Color.Blue));
    myObjectList.Items.Add(new MyObject(Color.LightSlateGray));

    XmlTextWriter writer = new XmlTextWriter("MyObjects.xml", null);
    writer.Formatting = Formatting.Indented;
    myObjectList.WriteXml(writer);
  }
}
 
<?xml version="1.0" ?>
<Items>
    <Item>BlanchedAlmond</Item>
    <Item>Blue</Item>
    <Item>LightSlateGray</Item>
</Items>
 
The sample is straightforward but you can use the same technique with data of any complexity.  "MyObjectList" contains a List<>, but that fact turns out to be immaterial.  The core idea is that implementing IXMLSerializable ReadXML() and WriteXML() can get the job done no matter what the data is or what format it needs to be saved as.  Here is the full source for the example, followed by an example with a slightly more complex xml structure.

using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Drawing;

namespace SerializeGenerics1
{

  public class MyObject
  {
    public MyObject(Color MyColor)
    {
      _myColor = MyColor;
    }

    private Color _myColor;
    public Color MyColor
    {
      get { return _myColor; }
      set { _myColor = value; }
    }
  }

  public class MyObjectList
  {
    List<MyObject> _items = new List<MyObject>();

    public List<MyObject> Items
    {
      get { return _items; }
      set { _items = value; }
    }

#region IXmlSerializable Members

    public System.Xml.Schema.XmlSchema GetSchema()
    {
      return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
      _items.Clear();
      while (!reader.EOF)
      {
        if (reader.ReadToFollowing("Item"))
        _items.Add(new MyObject(Color.FromName(reader.ReadString())));
      }
      reader.Close(); 
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
      writer.WriteStartDocument();

      writer.WriteStartElement("Items");

      foreach (MyObject myObject in _items)
      {
        if (myObject.MyColor.IsKnownColor)
        writer.WriteElementString("Item", myObject.MyColor.Name);
      }

      writer.WriteEndElement(); // close Items tag

      writer.WriteEndDocument();

      writer.Close();
    }

#endregion
}

class Program
{
  static void Main(string[] args)
  {
    MyObjectList myObjectList = new MyObjectList();

    XmlTextReader reader = new XmlTextReader("MyObjects.xml");
    myObjectList.ReadXml(reader);

    myObjectList.Items.Clear();
    myObjectList.Items.Add(new MyObject(Color.BlanchedAlmond));
    myObjectList.Items.Add(new MyObject(Color.Blue));
    myObjectList.Items.Add(new MyObject(Color.LightSlateGray));

    XmlTextWriter writer = new XmlTextWriter("MyObjects.xml", null);
    writer.Formatting = Formatting.Indented;
    myObjectList.WriteXml(writer);
  }
 }
}

This next example serializes settings for a "Wheel of Fortune" style game written in WPF.   The "Color" referenced here is from the Systems.Windows.Media namespace and doesn't have a "Name" property as in the previous example.  The xml file is nested with a Settings/Setting/<properties> structure:

<?xml version="1.0" ?>
<Settings>
  <Setting>
    <Percentage>10</Percentage> 
    <Color>#FFFF0000</Color> 
    <Title>Nice Try</Title> 
    <Description /> 
    <IsWinner>False</IsWinner>
  </Setting>
  <Setting>
    <Percentage>10</Percentage> 
    <Color>#FFB0E0E6</Color> 
    <Title>T Shirt</Title> 
    <Description>T-Shirt</Description> 
    <IsWinner>True</IsWinner> 
  </Setting>
</Settings>

The IXmlSerializable implementation:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Collections.ObjectModel;
using System.Windows.Media;
using System.ComponentModel;
using System.Diagnostics;

namespace Falafel.Training.WPF
{
  public class Settings : IXmlSerializable, IEnumerable
  {
    List<Setting> _items = new List<Setting>();

  public void Add(Setting setting)
  {
    _items.Add(setting);
  }

#region IXmlSerializable Members

  public System.Xml.Schema.XmlSchema GetSchema()
  {
    return null;
  }

  public void ReadXml(System.Xml.XmlReader reader)
  {
    try
    {
      XmlTextReader textReader = reader as XmlTextReader;
      textReader.WhitespaceHandling = WhitespaceHandling.None;

      _items.Clear();

      while (textReader.ReadToFollowing("Setting"))
      {
        Setting setting = new Setting();
        textReader.Read();
        setting.Percentage = Convert.ToInt32(textReader.ReadString());
        textReader.Read();
        setting.Color = (Color)TypeDescriptor.GetConverter(typeof(Color)).ConvertFromString(textReader.ReadString());
        textReader.Read();
        setting.Title = textReader.ReadString();
        textReader.Read();
        setting.Description = textReader.ReadString();
        textReader.Read();
        setting.IsWinner = Convert.ToBoolean(textReader.ReadString());
        _items.Add(setting);
      }
    }
  catch (Exception ex)
  {
    throw new ApplicationException("Unable to open Settings file. " + ex.Message);
  }
  finally
  {
    reader.Close();
  }
}

public void WriteXml(System.Xml.XmlWriter writer)
{
  XmlTextWriter textWriter = writer as XmlTextWriter;
  textWriter.Formatting = Formatting.Indented;
  textWriter.WriteStartDocument();
  textWriter.WriteStartElement("Settings");

  foreach (Setting setting in _items)
  {
    string color = TypeDescriptor.GetConverter(typeof(Color)).ConvertToString(setting.Color);

    textWriter.WriteStartElement("Setting");

    textWriter.WriteElementString("Percentage", setting.Percentage.ToString());
    textWriter.WriteElementString("Color", color);
    textWriter.WriteElementString("Title", setting.Title);
    textWriter.WriteElementString("Description", setting.Description);
    textWriter.WriteElementString("IsWinner", setting.IsWinner.ToString());

    textWriter.WriteEndElement(); // close Setting tag
  }

  textWriter.WriteEndElement(); // close Settings tag
  textWriter.WriteEndDocument();
  textWriter.Close();
}

#endregion

#region IEnumerable Members

  public IEnumerator GetEnumerator()
  {
    return _items.GetEnumerator();
  }

#endregion
 }
}

ReadXML() casts XMLReader to a XMLTextReader to consume the WhiteSpaceHandling property.  Likewise, WriteXML casts XMLWriter to XMLTextWriter to use the Formatting property.   The structure used here of Settings/Setting/<some properties> is entirely arbitrary.  You can read and write to and from any xml structure you care to put together.

 | 
posted on November 7, 2006  #    by Noel Rice  Comments [0]