Monday, April 21, 2008
Some time ago, Stuart Kent published the roadmap for DSL Tools. The idea of Dsl extensibility and WPF-based design surface mentioned there easily make someone drool. In the comments to the same post, he also mentions a way to provide your own editor instead of the built-in one. Here's my take on using that technique to provide a custom editor-wannabe in WPF.
  • Create a new Domain-Specific Language Designer (File \ New \ Project \ Other Project Types \ Extensibility)
    Name the project as WPFDSLDesigner
  • Select Class Diagrams template and click Finish to accept the defaults.
  • On the DSL Explorer tool window, click on the Editor node. Make a note of the FileExtension property in the Properties Window. Right click Editor node and Delete it.
  • Right click on the WPFDSLDesigner root node and select Add New Custom Editor. Set the FileExtension property. Set Root Class property to ModelRoot.

  • Transform All Templates using the rightmost button on top of the Solution Explorer.
  • Try building the project. The compiler error will lead you to the customization point. The cool thing with the DSL Tools is that, it clearly marks customization points expected by the developer with appropriate comments, all the time. We will add a partial class to supply our own getter in this case, as described by the comment in the code.
  • Add a project reference to WindowsFormsIntegration assembly (The last element in the dialog, most probably)
  • Add a class named WPFDesignerDocView to the DSLPackage project. (I created a DocView folder to group added files in a single place.) Change the code as follows:

    namespace Company.WPFDSLDesigner.DslPackage

    {

        using System.Windows.Forms;

        using System.Windows.Forms.Integration;

     

        internal partial class WPFDSLDesignerDocView

        {

            private ElementHost host;

            public override IWin32Window Window

            {

                get

                {

                    if (host == null)

                    {

                        host = new ElementHost { Dock = DockStyle.Fill };

     

                        WPFDesigner designer = new WPFDesigner(this);

                        host.Child = designer;

                    }

     

                    return host;

                }

            }

     

            protected override bool LoadView()

            {

                bool result = base.LoadView();

                if (result)

                {

                    ((WPFDesigner)host.Child).DocumentLoaded();

                }

     

                return result;

            }

        }

    }

  • Add a User Control (WPF) item to the project, name it WPFDesigner. Change the Xaml code of the UserControl as follows:

    <UserControl x:Class="Company.WPFDSLDesigner.DslPackage.WPFDesigner"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

        <UserControl.Resources>

            <Style TargetType="{x:Type ListBox}">

                <Setter Property="ItemsPanel">

                    <Setter.Value>

                        <ItemsPanelTemplate>

                            <StackPanel />

                        </ItemsPanelTemplate>

                    </Setter.Value>

                </Setter>

                <Setter Property="ItemTemplate">

                    <Setter.Value>

                        <DataTemplate>

                            <Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Margin="6">

                                <Label Content="{Binding Path=Name}"></Label>

                            </Border>

                        </DataTemplate>

                    </Setter.Value>

                </Setter>

            </Style>

        </UserControl.Resources>

     

        <Grid>

            <ListBox Name="modelClassViewer" ItemsSource="{Binding Mode=OneWay}" />

        </Grid>

    </UserControl>

  • And change the code of the UserControl to:

    namespace Company.WPFDSLDesigner.DslPackage

    {

        using Microsoft.VisualStudio.Modeling.Shell;

        using System.Collections.Generic;

     

        public partial class WPFDesigner

        {

            public ModelingDocView DocView { get; set; }

            protected ModelRoot root;

     

            public WPFDesigner()

            {

                InitializeComponent();

            }

     

            public WPFDesigner(ModelingDocView docView)

                : this()

            {

                DocView = docView;

            }

     

            public void DocumentLoaded()

            {

                if (DocView != null && DocView.DocData.RootElement != null)

                {

                    // Had some problems binding to ModelRoot.Types directly. Using a custom list instead.

                    var list = new List<ModelType>();

                    list.AddRange(((ModelRoot)DocView.DocData.RootElement).Types);

                    modelClassViewer.DataContext = list;

                }

            }

        }

    }


  • Your final project structure should be looking something like this:

  • Run the solution to open the Debugging project. Open the Sample.mydslx file. That's all.
And the result is:



Nothing fancy at the moment, but it's pure WPF. Add some compartment shapes and connectors, layout the entities rather than using a StackPanel and you're almost done. Nested shapes and such should be a breeze to implement.

Thank god they're working on this some future version of DSL Tools, WPF is so powerful to customize. I'm expecting that in the final version, we'll optionally be able to provide our own templates instead of the generated ones for customize-like-hell experiences.

Here are some future research points for the implementation above:
  • We can write a code generator to create shapes based on the Diagram Element properties in DslDefinition.dsl. Compartments, decorators, appearance properties etc. Why enumerate a bunch of types at runtime when we're already generating the rest design time?
  • Databinding to LinkedElementCollection failed pretty bad, VS kept crashing in every single case. Hey, I want to use built-in collections. Any INotifyCollectionChanged, by the way?
  • We should be mapping WPF menu items to package menus (VSCT). Add new property, validate etc.
  • We should either use the original .designer file structure to persist the layout etc. or find a way to store that data somewhere. (Designer file is the best)
  • Performance is not so great. I tried some 3D and it's even worse, much worse. There's something fishy going-on when you host a WPF control on top of a win32 container, or in VS, or both.
posted on 4/21/2008 2:55:54 PM UTC  #    Comments [3]
 Thursday, June 14, 2007
Preview 3 has this functionality, but there are a few things to mention.

ActiveWriter use ActiveRecord assembly to generate NHibernate config files. So you'll need Castle.ActiveRecord.dll, NHibernate.dll and all necessary dependincies to make it work.



  1. Set the target to NHibernate
    This way, AW will generate .hbm.xml files for each entity in your model.
  2. Set the Assembly Path to where Castle.ActiveRecord.dll, NHibernate.dll and all necessary dependincies reside.
    The design decision was that, I didn't want to package these assemblies with AW. Rather than that, I thought user will most probably have them so they can choose whatever version they want to generate the configuration. If you don't supply the path, AW (VS, actually) will look for them in GAc and then {Your VS Path}\Common7\IDE\Castle.ActiveRecord.dll, so if you have them in one of those places, it should be fine.

    One other quirk is, if you first try to generate with the wrong path, setti ng the correct one later won't work until you restart Visual Studio. This is the framework's limitation, once you try to load an assembly and get an error, the result will be cached for subsequent tries. So VS appdomain should be restarted to make it work. I'll have a possible soliton for this for a future version of ActiveWriter (will try to load in a dummy appdomain, then in the VS appdomain)

    You may use the fully qualified assembly names for Active Record Assembly Name and NHibernate Assembly Name to target a specific version in the GAC, if you have more than one in there.
  3. When you save the model, AW will generate configuration.
I'll prepare a better documentation in the wiki.

Have fun.

Update:

AW does not work with release version of Castle assemblies (RC2?) for NHibernate generation, it works with the trunk (or with recently compiled assemblies). You can use the latest bits from the build server: http://builds.castleproject.org/cruise/index.castle
posted on 6/14/2007 11:46:32 AM UTC  #    Comments [29]

This was sitting on the trunk for a while because of a bug in NHibernate config generation. I believe it's fixed now, so let's see if it works :)

First of all, this release is trying to be compatible with Castle trunk, it may generate code usable by released Castle components but it follows recent changes (might miss a few days). You know, Castle will be 1.0 one day and ActiveWriter will be a release, not a preview, in that day.

And I'm trying create some documentation on using.castleproject.org (Castle's wiki) to make Hammett stop whining about the lack of documentation of contrib projects (kidding!). If you're using ActiveWriter and want to share your tips, tricks, workarounds or anything about it, it would be great if you contribute to the documentation.

The most important update is the direct NHibernate configuration support. Now you can instruct ActiveWriter to generate NHibernate configuration files (.hbm.xml) for each entity in your model. Generated classes won't have ActiveRecord attributes in this case. More on this in a later post (Update: here).



Another interesting change is the use of Castle.Components.Validator instead of the ActiveRecord validators. AR changed in the trunk to use it, so does ActiveWriter.



One more exiciting news is, you can drag tables of MySQL from Server Explorer onto the modelin surface (see Michael's patch below).

Full list of changes below. A big thanks to all who sent patches, ideas and bug reports.

New:
  • Now optionally generates NHibernate hbm.xml files.
  • Namespace in generated code is now customizable (Idea: Robert van Hoornaar)
  • Imports in generated code is now customizable (Idea: Robert van Hoornaar)
  • Model classes can override model level base class definition. (Patch: Robert van Hoornaar)
  • Model classes can override model level generics generation. (Idea: Robert van Hoornaar)
  • Contrib-26: Add support for AR Nested / NH Component mappings. Allow the ability to specify a column prefix for active record. (thx: Adam Tybor)
  • Optionally generates classes implementing INotifyPropertyChanged.
  • Support for drag-drop from Server Explorer for MySQL databases. Requires MySQL Connector/Net (5.1) (included). (Patch: Michael Morton)
  • Support for custom types through IUserType. See http://support.castleproject.org/browse/CONTRIB-28#action_11456 for usage. (Patch: Ricardo Stuven)
  • Ability to define a nested class with a different property name than the nested class (Idea: Craig Neuwirt)
Fixed:
  • Contrib-23: Produces CascadeEnum instead of ManyRelationCascadeEnum
  • Ability to generate virtual properties to support Lazy properly (thx: Ayende)
  • Make sure that Char and AnsiChar types are treated as a System.String types, instead of System.Char types (patch: Ayende)
  • HasAndbelongToMany does not take custom property names and not found behavior into account.
  • Can’t add a Many To Many relationship to 2 entities when the Class and Table names are different.
  • Column keys in many-to-one relations is not optional. (Patch: Robert van Hoornaar)
  • Partially fixing NHibernate code generation errors. Now works with a workaround.
  • Nullable types only when NotNull=false (Patch: Ricardo Stuven)
  • Changed to Castle.Components.Validator
And just one more thing. Someone asked me if ActiveWriter is a competitor for Linq to SQL. AW is just a servent of the heavyweight champion, NHibernate/AR combo. Linq to SQL is competing with NHibernate. I didn't have time to examine Linq to SQL designer, but it's built using DSL Tools just like ActiveWriter so some functionality should be alike. Other than that it's the framework, not the tools, important.
posted on 6/14/2007 8:29:25 AM UTC  #    Comments [9]
 Wednesday, May 09, 2007
Ayende, in his post on virtual entry barriers / learning curve of Castle pieces, said:
One of the main differences between OSS and Commercial software is the amount of time that is invested in the frills. Active Record and NHibernate has a designer, which is nice, but it is not a level 1 priority to any of the people using either Active Record or NHibernate. The reason is that both those frameworks were built knowing that a designer is not something that you should need in order to work with them.
That is, of course, super true for MonoRail and when I started working on ActiveWriter, I said "ActiveRecord is so simple, no one gonna use a code generator for it". Now I think somewhat different, in the context of O/RM designers.

When I think of working with ActiveRecord, I see it in two different parts. First is the sceleton of an entity; all the attributes tagging the class, properties and fields to describe the model to ActiveRecord and then NHibernate, eventually.

    [ActiveRecord()]

    public partial class Entity : ActiveRecordBase {

 

        private int _key;

 

        [PrimaryKey(PrimaryKeyType.Native, ColumnType="Int32")]

        public int Key {

            get {

                return this._key;

            }

            set {

                this._key = value;

            }

        }

    }


This code is a no brainer to write and AR has quite good documentation to get you there easily. But it's so repetative that it's the perfect place for using a designer. I don't say that people using ActiveRecord should forget all the attributes to define an entity alltogether, but once you studied the underlying workings of the framework I believe even the most basic framework can use a designer when it comes to the brute force part of the code. After all, you are designing some entitities and that's what a designer do best.

One problem here is that it's not easy to modify this kind of generated code. Thanks to 2.0, partial classes help great when adding new functionality, but it's impossible to modify generated getters and setters. DSL Tools is using the term "double derived" for the approach used to solve this problem, use an inherited empty entity instead of the original one around to make it possible to override properties. This model works fine in DSL Tools context but might make real world entities complicated. This one and solutions like this which makes code more bloated are, I believe, the dark side of code generating designers.

There is also the process of giving the entity some brains; validation, query helper etc. I, again, can see the value of a designer here. ActiveWriter can generate entities with ActiveRecord validators (and there's a JIRA entry waiting to port it to Castle validators). Thanks to his permission, I'll integrate Ayende's Generator into ActiveWriter to make it generate code with all the typed query functionality but ActiveWriter of course won't be writing the query.

The other part of working with ActiveRecord is knowing what the framework gives you, what does it do on behalf, what are the list of things to know beforehand to know before it starts persisting. You should know about lazy loading, order of saving, inverse relations etc. This is the point when you instruct the framework to manage what you designed before, and I think where a designer should keep it's hands off. ActiveWriter, and tools alike, should help developers to define metadata. They should not act like metadata manipulators to do the programming. I consider Ayende's comments above true in this part of the usage. A good O/RM designer should work without getting the way, and shouldn't make the user careless by doing the thinking on behalf.

posted on 5/9/2007 2:42:35 PM UTC  #    Comments [10]
 Monday, January 29, 2007
It sometimes get frustrating when you debug your project through another instance of Visual Studio, especially if you manipulate the "active" DTE within your debug project. I somehow ended up to the following when getting a reference to DTE:

System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.8.0");


It worked most of the time, but "not all the time" means you may add a project to the solution where your code resides (the parent VS instance), rather than where the debugging session is. So, after some research, DTE code turned into this:

[DllImport("ole32.dll")]

public static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);


[DllImport("ole32.dll")]

public static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);


[CLSCompliant(false)]

public static DTE GetDTE(string processID)

{

    IRunningObjectTable prot;

    IEnumMoniker pMonkEnum;

 

    string progID = "!VisualStudio.DTE.8.0:" + processID;

 

    GetRunningObjectTable(0, out prot);

    prot.EnumRunning(out pMonkEnum);

    pMonkEnum.Reset();

 

    IntPtr fetched = IntPtr.Zero;

    IMoniker[] pmon = new IMoniker[1];

    while (pMonkEnum.Next(1, pmon, fetched) == 0)

    {

        IBindCtx pCtx;

        CreateBindCtx(0, out pCtx);

        string str;

        pmon[0].GetDisplayName(pCtx, null, out str);

        if (str == progID)

        {

            object objReturnObject;

            prot.GetObject(pmon[0], out objReturnObject);

            DTE ide = (DTE)objReturnObject;

            return ide;

        }

    }

 

    return null;

}

posted on 1/29/2007 10:17:43 PM UTC  #    Comments [0]
 Saturday, September 30, 2006

Help and download available here.

If you missed the announcement, it is a visual designer, an addin for Visual Studio 2005, to design a domain model and to generate code decorated with ActiveRecord attributes.

Please send bugs, suggestions and feature request through the Navigation pane of the site.

Thanks To

ActiveRecord team for the great and well supported library, NHibernate and Hibernate community for making this chain-reaction possible, Microsoft DSL Tools team for making DSL modelling this easy, everyone at DSL Tools forum, and my wife Damla for her patience.

Pluralization code is simplified version of Damian Conway's algorithm in paper An Algorithmic Approach to English Pluralization
Server Explorer integration greatly inspired from Ted Glaza's post here.
posted on 9/30/2006 6:12:03 PM UTC  #    Comments [13]
 Friday, September 15, 2006

Head to the VSIP member site to download. I'm %46 done right now.

Since DSL Tools v1 is bundled with this final release, and since I'll finally be able to include redistributables with ActiveWriter installer, I'm twice excited right now :)

My plan is:

    Compile ActiveWriter against DSL Tools v1
    Fix things broken by the v1
    Add one2one support (Just manual modelling. No heuristics for drag-drop. Will complete that later)
    Test & release.

 And about the current state of the project:

    Handles many-to-one, both in manual design and drag-drop.
    Handles many-to-many, both in manual design and drag-drop.
    Primary keys, properties, fields.
    Composite keys: Writes the attribute as well as a separate composite key class :) Yup.
    Some more model validators (If PrimaryKeyType.Sequence, then sequence name should be defined etc.)
    Versions and Timestamps.
posted on 9/15/2006 12:31:35 PM UTC  #    Comments [4]
 Monday, August 28, 2006
I use CodeDom to generate ActiveRecord classes in ActiveWriter. Quite fancy, but gets complicated easily. To generate something like this:

        public override bool Equals(object obj)

        {

            if (obj == this) return true;

            if (obj == null || obj.GetType() != this.GetType()) return false;

            MyCompositeKey test = (MyCompositeKey)obj;

            return (_keyA == test.KeyA || (_keyA != null && _keyA.Equals(test.KeyA))) &&

              (_keyB == test.KeyB || (_keyB != null && _keyB.Equals(test.KeyB)));

        }


you code this:

        private CodeTypeMember GetCompositeClassEqualsMethod(string className,  List<CodeMemberField> fields)

        {

            CodeMemberMethod equals = new CodeMemberMethod();

            equals.Attributes = MemberAttributes.Public | MemberAttributes.Override;

            equals.ReturnType = new CodeTypeReference(typeof(Boolean));

            equals.Name = "Equals";

 

            CodeParameterDeclarationExpression param = new CodeParameterDeclarationExpression(typeof(Object), "obj");

            equals.Parameters.Add(param);

 

            equals.Statements.Add(new CodeConditionStatement(

                                      new CodeBinaryOperatorExpression(

                                          new CodeFieldReferenceExpression(null, "obj"),

                                          CodeBinaryOperatorType.ValueEquality, new CodeThisReferenceExpression()

                                          ), new CodeMethodReturnStatement(new CodePrimitiveExpression(true))

                                      )

                );

 

            equals.Statements.Add(new CodeConditionStatement

                                      (

                                      new CodeBinaryOperatorExpression

                                          (

                                          new CodeBinaryOperatorExpression(

                                              new CodeFieldReferenceExpression(null, "obj"),

                                              CodeBinaryOperatorType.ValueEquality, new CodePrimitiveExpression(null)),

                                          CodeBinaryOperatorType.BooleanOr,

                                          new CodeBinaryOperatorExpression(

                                              new CodeMethodInvokeExpression(new CodeFieldReferenceExpression(null, "obj"), "GetType"),

                                              CodeBinaryOperatorType.IdentityInequality,

                                              new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "GetType"))

                                          )

                                      , new CodeMethodReturnStatement(new CodePrimitiveExpression(false))

                                      )

                );

 

            equals.Statements.Add(

                new CodeVariableDeclarationStatement(new CodeTypeReference(className), "test",

                                                    new CodeCastExpression(new CodeTypeReference(className),

                                                                            new CodeFieldReferenceExpression(null, "obj"))));

 

            List<CodeExpression> expressions = new List<CodeExpression>();

            foreach (CodeMemberField field in fields)

            {

                expressions.Add(

                    new CodeBinaryOperatorExpression(

                        //_keyA == test.KeyA

                        new CodeBinaryOperatorExpression(

                            new CodeFieldReferenceExpression(null, field.Name),

                            CodeBinaryOperatorType.ValueEquality,

                            new CodeFieldReferenceExpression(new CodeFieldReferenceExpression(null, "test"), field.Name)),

                        CodeBinaryOperatorType.BooleanOr, // ||

                        new CodeBinaryOperatorExpression(

                            //_keyA != null

                            new CodeBinaryOperatorExpression(

                                new CodeFieldReferenceExpression(null, field.Name),

                                CodeBinaryOperatorType.IdentityInequality,

                                new CodePrimitiveExpression(null)

                                ),

                            CodeBinaryOperatorType.BooleanAnd, // &&

                            // _keyA.Equals( test.KeyA )  

                            new CodeMethodInvokeExpression(

                                new CodeFieldReferenceExpression(null, field.Name), "Equals",

                                new CodeFieldReferenceExpression(

                                    new CodeFieldReferenceExpression(null, "test"), field.Name))

                            )

                        )

                    );

            }

 

            CodeExpression expression = null;

            if (expressions.Count > 2)

                expression =

                    new CodeBinaryOperatorExpression(expressions[0], CodeBinaryOperatorType.BooleanAnd, GetBooleanAnd(expressions, 1));

            else

                expression = new CodeBinaryOperatorExpression(expressions[0], CodeBinaryOperatorType.BooleanAnd, expressions[1]);

 

 

            equals.Statements.Add(new CodeMethodReturnStatement(expression));

 

            return equals;

        }


Strangely, there's no CodeBinaryOperatorType.ValueInequality operator defined. (a==b)==false will do the trick, but how about not having an XOR? I find CodeDom's lack of completeness... disturbing.

posted on 8/28/2006 9:46:29 PM UTC  #    Comments [4]
 Saturday, August 19, 2006

ActiveWriter is a DLinq designer like addin for Visual Studio 2005 to design a domain model and to generate code decorated with ActiveRecord attributes.

 

 

It supports / will support:

  • Modeling
    Classes (Almost done)
    Class properties (Almost done)
    Setting a property as primary keys (Done)
    Setting more than one property as composite key (TBI)
    Many to One (Done), One to One, Many to Many relations (TBI)
    Nested classes (TBI)
    ... and more (I'm targeting to support the whole ActiveRecord model)
  • Model validation
    Current build validates most common things like classes without names, spaces in class names etc.
  • Drag and drop of table(s) from Server Explorer
    Can place tables, populate properties. I'm working on relations right now.
  • Automatic generation of source code of the model on save.
    Not implemented yet, but I know how to do it. Right now, it goes through .tt file.
  • Multiple database types as drag/drop source.
    Working on SQL Server right now. Oracle and others will follow.

I use the current CTP of DSL Tools to build the base. There are no downloadable bits right now, since I have to get a VSIP licence to make it run without the SDK.

I don't know to what extent I can open the source. I'll sure make the source downloadable but since part of the code is generated by DSL Tools and there's this VSIP licence, I may not be able to licence it under BSD or something. I'll look for it.

Anyway, I believe it will be a nice addin to have for people working with ActiveRecord/NHibernate. I'm doing my best to release a preview in one or two months.

Comments and suggestions are welcome.


posted on 8/19/2006 11:02:33 AM UTC  #    Comments [8]

...but Foreign Key support is just useless, especially for SqlConnection. Just look at the table signature:

ColumnName DataType Description

constraint_catalog

String

Catalog the constraint belongs to.

constraint_schema

String

Schema that contains the constraint.

constraint_name

String

Name.

table_catalog

String

Table Name constraint is part of.

table_schema

String

Schema that that contains the table.

table_name

String

Table Name

constraint_type

String

Type of constraint. Only "FOREIGN KEY" is allowed.

is_deferrable

String

Specifies whether the constraint is deferrable. Returns NO.

initially_deferred

String

Specifies whether the constraint is initially deferrable. Returns NO.

Quite disappointing, since there's no sign of columns involved. And there's no other GetSchema() way to get them. OracleConnection, on the other hand has a collection called ForeignKeyColumns, which returns columns included for a given FK relationship in a given table. Once you have the FK relation, you should query both sides to get the full column list, though, but better than nothing.

So, with SqlConnection, I use sp_fkeys in ActiveWriter. I wish I could just use GetSchema(), to keep things simple. Sad.

posted on 8/19/2006 8:59:42 AM UTC  #    Comments [0]
 Thursday, February 09, 2006

Please Note: If you're using VS 2008, you don't need this tool anymore. 2008 has this functionality built-in, look for the dropdown on top of the resource editor.

Introduction

In Visual Studio 2005, strongly-typed code for resource files (.resx files) are automatically generated when you save them. The generated class, however, cannot be accessed externally since the class is marked as internal.

This little add-in just instructs the generation process to build a Public class. To use it, just change the Custom Tool property of any resource file from ResXFileCodeGenerator to ResXFilePublicCodeGenerator.

After you make any changes and save the file, IDE will auto-generate a Public strongly-typed class for your resource.

Licence

The component and source is provided "as is" and there are neither warranties to the quality of the work nor any expressed or implied agreements that the programmer will release any updates. The programmer will release updates and provide any support at his own discretion.

External code mentioned in credits may subject to their own licence terms.

1.0.0.1 Update:
Fixed the issue with VB.Net root namespaces. This is, in fact, reported months ago but I totally forgot to fix it. I apoligize from all VB.Net users for the trouble.

Download

v1.0.0.1

Credits

Includes some code from Daniel Cazzulino’s XSD -> Classes Generator Custom Tool article.

VS Integration classes from http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=4AA14341-24D5-45AB-AB18-B72351D0371C

posted on 2/9/2006 12:10:42 AM UTC  #    Comments [13]