Sunday, December 14, 2008

Simplify Inline Editing using GridView, GridViewControlEmbedder ASP.NET AJAX Extender.

I created another nice ASP.NET AJAX extender for GridView.

With it's help you can enable inline editing for Grids in web pages relatively faster, comparing to time needed to implement this feature from scratch using pure JavaScript.

For example having the following GridView on the page:

image

You can add an extender that allows editing of values of cells right in place:

image

Extender has inner elements named "GridViewControlEmbedderColumn", each of this elements enabled inline editing for one column in the GridView. ColumnIndex attribute points to client side cell index - for example 0 will point to "First Name" column. ControlType can have one of several values. For now extender supports two types of embedded controls for cells - "TextBox" and "ComboBox". CssClass attribute is optional and if you set it to some css class it will be assigned to embedded control when it's created.

Extender itself has several important properties and events.

HoverCssClass - set here the css class that will be assigned to cell's in GridView when they are hovered (hover css class will work only for cells having embedded control assigned to them).

TargetControlID - set here ID of target GridView control, this extender should extend.

RowIdAttributeName - set here name that will be used as name for client side attribute when GridView rows are rendered. It is important to understand that values that will be entered into each cells are identified using column index and row id - some data-driven value that will identify row uniquely. The ID itself can be set in event handler for OnGridViewControlEmbedderRowReady event (see below).

OnGridViewControlEmbedderRowReady - subscribe to this event to set row IDs initial cell values for each row and cells in it before GridView renders. Client side script has to know this values to correctly populate/accumulate values that will be later available on server side after postback.

For the sample markup showed above this event handler will look like this:

image 

Lets review this code in more details.

First line of code just gets to the DataItem property of the GridView Row, in the sample I use XmlDataSource and this code allows to get to the original XPathNavigator to be able to retrieve values from the datasource for that row. It's important to mention that this event is raised for each row of GridView in synch with OnRowDataBound event raised originally by GridView. So in this event handler you can do the same manipulation over row/cells as you could in OnRowDataBound event handler.

Now there are two properties of EventArgs object that need to be set. These are e.RowID and e.CellValues.

Set some unique value to e.RowID (type of string) - this will be used later to retrieve edited values in the grid.

Also set initial values for cells to e.CellValues collection (dictionary<int, string>) - they will be used on the client side initially on first inline editing operation for each cell.

 

The result of using three small code blocks showed above is show on this picture:

image

Vertical scroll appeared for our GridView thanks to another extender I created earlier (Cross Browser GridView Fixed Header Extender, ASP.NET AJAX)

As you see You can simply click any cell and edit it's value - extender allows to do it without going to the server - all this functionality is done on the client.

Now when you do postback (for example user clicks "Save" button), you have access to all values that user have edited in "Values" property of extender.

This property is of type "Dictionary<string, Dictionary<string, string>>" - here outer dictionary's keys are row ids and inner dictionary's keys are cell indexes.

For example using this code we can handle / validate / and process all cell values edited by the user:

image

so for example let's edit two rows in the grid an click "Submit":

image

 

Extender has nice API on the client side as well. It allows to control the process of cells editing, and allows to simplify really complex scenarios.

Please review GridViewControlEmbedder.js file and especially functions inside "Public methods" region.

Extender has two client side events, to which you can subscribe to control/validate editing on the client side:

beforeCellEdit - occurs before cell is entered for edit.

afterCellEdit - occurs before cell is left and it's edited value is committed to the client-side state.

You can subscribe to these events in javascript using simple syntax:

image

where c.ce in string - ClientID of the extender.

Extender passes custom event args passed to the event handlers .

Constructors look like this:

image

 

As you can see you can cancel entering or exiting the cell by setting cancel property of event args. You can analyze the information about currently edited cell, row, column index, inner text of cell, also you have a reference to the grid from there. There is also "callBack" property that can be used to perform some asynchronous operation before continuing cell edit. For example you can set cancel property of event agrs to true to prevent continuing of current operation (entering/exiting cell), issue request to the server, and only after getting response you can call e.get_callBack() property - which is pointer to the function in the extender that will complete the operation.

I also created second demo page that shows how to use client side events of the extender to use inline editing with ComboBoxes. Here is the screenshot:

image

Demo page demonstrates using of drop down lists for choosing options for each cells. what options will be shown for what cell or row is pretty controllable from javascript code. Here is the code snippet from this demo page:

image

Here I call web service for options, passing it column index, then I set cancel property of event args to true to prevent entering the cell. After response is get back from the server, I grab the reference to the select element (e.get_control()) and fill it with options containing information got from the server. Finally I call callBack provided again in event args to enter cell and allow user to edit it.

 

Finally here is a full source code for the control and demo pages. Download and use it,

Hope this helps.

 

P.S.

I am thinking about moving all my control's source code - I write about in this blog, to the Codeplex project. This version of controls library also contains some fixed and upgraded controls from earlier blog posts - HoverTooltip, GridViewFixedHeaderExtender. I think I will move the source to the Codeplex about next week. Also I will update all old blog posts to link to Codeplex project for controls suite download.

kick it on DotNetKicks.com

Sunday, November 9, 2008

Code generation technique using MS codename "Oslo", T4 templating engine and VS Custom tool

You can download full source code for this post here.

Not long time ago Microsoft announced new product named "Oslo".

Quote from Microsoft Oslo Developer Center:

"Oslo" is the code name for our platform for model-driven applications. The goal of "Oslo" is to provide a 10x productivity gain by making model-driven applications mainstream with domain-specific models, a new language, and tools.

I will not go into much details about "Oslo" Modeling Platform, as I cannot define it better than "Oslo" SDK

I will quote one more definition from MS Developer Center, "Oslo" modeling platform contains the following parts:

  • A visual design tool (Microsoft code name “Quadrant”) that enables people to design business processes with well-understood, flowchart-like graphics; developers to design applications and components that comply with the requirements of those processes; and both to move from one view back and forth to observe the effect any changes in either place have on the overall validity of the application or business process. For more information, see "Quadrant".
  • A modeling language (Microsoft code name “M”) that makes it natural to extend system-provided models (such as Windows Communication Foundation (WCF) or Windows Workflow Foundation (WF) models) or create your own models for use on the “Oslo” modeling platform. For more information, see "M".
  • A SQL Server database (the code name “Oslo” repository) that stores models as SQL Server schema objects and model instance data as rows in the tables that implement the schema. This data is available to “Quadrant” and any other tool or data-driven application that can make use of it (and that has the appropriate permissions to do so). Whether models or model instance data is created visually, using “M”, or using any SQL data access API (for example, ADO.NET, EDM, OLE-DB, and so on) creating models and storing them in the “Oslo” repository enables future applications to examine and manipulate not only data structures used by applications but – because applications are modeled – the applications themselves, as they run. If data-driven application has enough detailed model information, applications can run without recourse to static compilation. For more information, see "Oslo" Repository.
  • However in the text above taken from "Oslo" overview there is no a single word about M Grammar - another cool part in "Oslo" product.

    Most of all I was exited with this modeling language MG (M Grammar). It can be used to create your own textual DSL-s (Domain Specific Languages), here is definition again from MSDN:

    The MGrammar Language (Mg) was created to enable information to be represented in a textual form that is tuned for both the problem domain and the target audience. The Mg language provides simple constructs for describing the shape of a textual language – that shape includes the input syntax as well as the structure and contents of the underlying information. To that end, Mg acts as both a schema language that can validate that textual input conforms to a given language as well as a transformation language that projects textual input into data structures that are amenable to further processing or storage. The data that results from Mg processing is compatible with Mg’s sister language, The "Oslo" Modeling Language, "M", which provides a SQL-compatible schema and query language that can be used to further process the underlying information.

    In this post I will try to shortly (but I am afraid in very technical details) introduce some cool sample I created on the weekend.

    1) I created my own domain specific language using M Grammar language from Oslo SDK,

    2) Created T4 Templating Engine Custom Host that will allow me to use T4 Templating Engine to generate code artifacts by parsing the "Data Graph" created by M Grammar language parser and

    3) Used Visual Studio Custom Tool to make possible the generation of multiply artifacts for single DSL file and placing them under that file in Visual Studio files hierarchy.

    The Domain Specific Language that I use in this sample is simple enough to make the sample easily understandable, but in real company - or product-wide DSL the language may be more complex. M Grammar gives the powerful options to create complex languages. You can find some samples of languages created using M Grammar under "%ProgramFiles%\Microsoft Oslo SDK 1.0\Samples\MGrammar\Languages".

     

    What this sample does ?

    The sample allows to use DSL language input files in Visual Studio C# applications. The files will contain code written on language that I wrote using M Grammar.

    Just create new VS project, add new file(s) inside it with ".dslcontract" extension:

    image

    Now write down some code using syntax of our custom language:

    image

    image

    Now lets save that files and see what we get:

    image

    Cool isn't it?

    As you see for each Enum construct custom tool have generated separate file with different extensions and language-specific contents:

    image

    image

    Now what is nice about the approach I used to create this functionality is - you can fully control

    1) for what type of language high level abstraction the code artifacts will be generated (Enum in this case, but language can hold Class or some another constructs in the future)

    2) for what Mg image file the code artifacts will be generated (I will define what is Mg image file later in the post)

    3) what types of code artifacts will be generated  (extensions, number of files for each high level abstraction, here by "high level abstraction" I mean syntax rule named "HighLevelAbstraction" in my custom DSL - you can see the listing of my language a little farther in the post),

    4) what will be the contents of generated code files (this is controllable through T4 templates and custom T4 text templating host I use in the sample)

    I will describe in more details each of the points above.

    1) (for what type of language high level abstraction the code artifacts will be generated )

    If you go to "%CommonProgramFiles%\Saatec.Dsl.Contracts.Language" path (this is where VS custom tool looks for resources, for use in production you can use different path for resources) you will see the following files:

    image

    Here by creating a new T4 templates (files with *.tt extension) and naming them with Enum.*.tt I can instruct the custom tool to generate one more artifact for each "high  level abstraction" in my language named "Enum". See also point No 4 below for description what will be used "*" part of the file name for.

    2) (for what Mg image file the code artifacts will be generated )

    On the picture above you may have already noticed file named "Saatec.Dsl.Contract.mgx". This file can be replaced at any moment with updated Mg image file  of my custom language(which optionally will support new constructs, rules and high level abstractions). Current simple language grammar in textual form looks like this:

    image

    You can find it's listing in Subfolder named "LanguageDevelopmentTools":

    image

    Here I placed

    a) shortcut for "Intellipad" - tool used to create textual DSLs using M Grammar,

    b) Saatec.Dsl.Contract.mg - M Grammar file containing Language definitions - syntax etc for our DSL,

    c) Test.dsl.contract - just test input file that can be used to see what output graph is generated for a language,

    d) PackLanguage.bat file - the batch file that compiles language (.mg) file into mgx image file and places it in one hierarchy above (for use by our VS  custom tool). I will place a screen-shot of "Intellipad" tool in action later in the post.

    3) (what types of code artifacts will be generated)

    I already mentioned that by creating Enum.*.tt file it is possible to instruct custom tool to use this T4 template to generate artifact for "high level abstraction" in my language named "Enum". By specifying extension instead of "*" sign we can give instruction to out VS custom tool to generate file of that extension. For example after finding file "Enum.cs.tt" in this folder, custom tool will generate file "Abstraction name".cs file (MessageBoxButtons.cs in our example).

    4) (what will be the contents of generated code files)

    Template files support text templating syntax, and after processing by templating engine will generate output for each single "high level abstraction" in my language. T4 templates can (and in most cases will) be host-specific,(host specific and are able to get reference to the hierarchical Data Graph produced by DSL language parser. See more information about "hostspecific" directive here.

    Typical T4 template looks like:

    image

    This template file name is Enum.cs.tt - it means it will be used to generate C# code file for each Enum "high level abstraction" in my domain specific language. Also you can see that I use GetOption method in the Host to get reference to a Data Graph, saying honestly I don't know what for GetOption should be used normally, I just noticed this method and that it is not called from inside templating engine, and I decided it is good method to override to provide a mechanism for passing my Data Graph to T4 templates. As you see graph is referenced at this line:

    image

    In the custom host implementation I overridden this method to return DSL parser output data graph:

    image

    where the mContractGraph is of type ContractGraph:

    image

    This graph gives a hierarchical view that describes what was in input DSL code file. Now we can use many of cool features of T4 to create artifacts. To make this possible I used Custom Text Template Host which ensures correct work of templates and provides reference to data graph. We will look at more details about it's implementation later (May be in next post?).

    Now what about error handling? - Currently the sample dumps all error texts into file under .dslcontract file that is named similarly as contract but has .txt extension. You should check this file after generation to be sure that process went smoothly. The file will accumulate errors raised by DSL language parser and T4 templating engine. The errors that are caused by custom tool itself can be viewed by clicking "Run Custom Tool" explicitly:

    image

     

    What can be used this sample for ?

    You can create simple DSL languages and generate multiply artifacts for input code files written using syntax of that languages.

    First of all DSL gives an opportunity to create simpler syntax than in languages you want generate code for.

    Secondly you can generate multiply code files on several languages for single DSL code file. This can give productivity boost in some situations, when a lot of "plumbing" code is needed, or when you have to generate similar structure code on several languages. For example in the sample I generate enumerations in C# and javascript code. This can be done easily by writing down 20-40 symbols and hitting "Save" opposed to several hundreds you had to write in common situation.

    And finally DSL is language agnostic - it does not depend on C# syntax and any other, it is parsed and data graph is produced that can be used to generate specific language code as well as XML, JSON, DB or any other structure.

     

    Implementation

    The architecture of the sample can be presented as follows:

    image

    Sorry for my designer skills :)

    Ok, so data flows the following way:

    1) User clicks "Save" or "Run Custom Tool" on original file in Visual studio.

    2) Custom tool gets reference to caller item, its text contents, default namespace etc. Custom tool calls "Code Generator" passing to it input text and default namespace

    3) Code generator calls DSL language Parser to populate data graph. It passes to it only original file's input.

    4) DSL Language Parser searches for language package on the disk in predefined location (configuration-driven approach can be added later), loads it, parses the input text and iterates trough all nodes in the graph, thus populating the internal - more user friendly data structure that will be later used in T4 templates.

    5) collection of graphs for each abstractions in input file are return back to "Code Generator" block.

    6) Code Generator block searches the predefined place on hard disk to find T4 template file for each graph. Graphs contain information about what kind of abstraction the represent. By using string name of an abstraction we can find actual template that will be used then.

    7) Code Generation block instantiates new Text Templating Engine in new AppDomain, and passes to it graph, template filename and custom host reference, starting with this text generation process.

    8) Text generation process is repeated for each graph and each template found for each graph type.

    9) Collection of generated code strings is passed back to custom tool, which in its turn uses Visual Studio automation to generate multiply files on the disk and subordinate them to original file in Visual Studio project.

    10) Finally Visual Studio will get generate files, and is ready to build.

    First of all to be able to run a sample code you have to download Oslo SDK from Microsoft website.

    It will install all tools and assemblies that are needed to author and parse textual DSLs, most specifically M Grammar.

    You can then quickly review "M Grammar in a Nutshell" document that will be available under Oslo program shortcut.

    It is really simple to get in quickly to M Grammar, You will have to use Intellipad tools to create your own language. It has good features - like syntax highlighting, error messages for syntax errors in M Grammar language code and more.

    The tool in action looks like this:

    image

    Here the language itself is in the center pane,

    Left pane will contain sample input for our language and

    Right pane will contain graph, that will be generated after parsing input.

    If there are any errors in input code you will see their details in the lower pane.

    There are more advanced ways of using complex languages - like using multiply files, you can see documentation, and very nice samples that come with Oslo SDK.

    After we created a language file, we can package it to file with .mgx extension, it can be later used by our applications to parse user input. What I really liked about M Grammar - it gives an opportunity of parsing input dynamically - this means  that languages can be used at runtime in our applications.

     

    So now we have our language file created and packed, lets see the VS solution for the sample.

    image

    Solution consists of four projects:

    CustomTool - The project containing class that implements IVsSingleFileGenerator interface - and implements it's methods "Generate" and "GetDefaultExtension". That class is called by VS when we hit "save" on file using that custom tool. Custom tool needs special registry keys to be set to point to its location. Also custom tool assembly should be registered with regasm tool, to be visible to VS COM process. I created "Install.bat" file that makes this actions automatically for you after each build of a solution. Also I created "Uninstall.bat" file which is also called each time before "Install.bat" is called. You can manually execute those files to uninstall or install our custom tool.

    Generator - this project implements all functionality that is used to parse our DSL language.  It should not be registered in GAC, because it references Oslo assemblies, that I could not register in GAC by some reason to the moment of writing this post.

    Interface - this is assembly that contains shared classes and interfaces needed for intercommunication of another assemblies in the solution. It should be registered in GAC. (This is done automatically by Install.bat" file I mentioned above.

    Generator.Host - this project implements custom text templating host. It is used to execute/ generate T4 templates and provide them graph for code generation. You can see it's implementation yourself, I really was not expecting that implementing custom host to be so simple. I just overrided several methods and properties to make it work in environment and AppDomain separated from Visual Studio.

     

    Some notes for getting started with the sample:

    After successful build of a solution please copy this directory:

    image

    To the path: %CommonProgramFiles%\Saatec.Dsl.Contracts.Language

    This is were custom tool will search for templates and language.

     

    Install.bat and Uninstall.bat files that are registering custom tool can be found here:

    image

    If you find this sample good for production you will have to create VS setup project and get rid of bat and reg files and instead use features of the project.

     

    To debug the custom tool just start another Visual Studio Instance and attach to it from VS running custom tool solution. Now after hitting "Save" on file with our custom tool in the VS instance being debugged you can hit breakpoints in our custom tool code.

     

    For generation of multiply files inside VS I use the code provided by Adam Langley (see below the original article URL).

    I think the post is going too long - so here is the source code. Download and use it on your own risk :)

     

    These two articles helped me a lot in understanding how T4 works under the hood, and how multiply files can be generated using T4 and VS Custom tool:

    How to generate multiple outputs from single T4 template

    Creating a Custom Tool to Generate Multiple Files in Visual Studio 2005

    Thanks to authors Oleg Sych and  Adam Langley for great articles and source code.

     

    Happy programming!

    kick it on DotNetKicks.com

    Tuesday, August 26, 2008

    Auto Generate References for Controls Residing in Naming Containers, ASP.NET

    Currently I work on a big project with quite a big amount of UserControls in it. Many controls in the system have quite complex markup. And often I need to reference controls residing in the naming containers, or simply inside another controls.

    for example if you have the markup like this:

    image

    you cannot simply reference control name rb1 or rb2 from the code behind of that control. So I use private properties to declare that controls on the scope of the page:

    image

    This becomes a pain when you have to quickly work on the system. It is not good when you waste 20% of time to just creating markup and trying to reference all controls in the markup. It becomes even harder when you have hierarchy of multiply naming containers. For example suppose you have some custom TabControl, whish contains inside it another TabControl, then again and again.

    To reach the control that is placed deep inside hierarchy of naming containers you have to define all controls in the hierarchy.

     

    I decided to write some helper application that generates all private properties in the hierarchy, resolves their types and allows to make work done in seconds ;).

     

    For example after running this application on a User control with more than 500 lines and about 5-level deep hierarchy of naming containers inside it (see only little part of the markup on the picture)

    image

    after a second I have auto generated set of properties (see small part on the picture):

    image

    Also note that not only standard controls where resolved, but also controls that where dragged to this UserControl, and where placed inside naming containers:

    image

    The type of a control is also generated and is not somehow hardcoded / pre-configured before use of the application.

     

    Lets see the steps I need to go through to achieve the results.

    1) I click on the shortcut to the console Application which is on my quick launch panel on the desktop.

    2) I enter the filename of the UserControl I want to generate properties for:

    image

     

    3) The application searches through whole directory structure of my web project for the file. If there are several files with that name it allows to choose which one I meant, otherwise it goes to the process of parsing right away:

    4) After finishing it opens text file with all generated properties, which can be pasted into code of the UserControl:

    image

    The project consists of one cs file and three txt files with settings that are used when parsing UserControl. These files can be customised to setup the application for your custom project,set of controls.

    image

    Mainly you don't need to modify the txt files, they already contain correct values allowing you to generate properties for any ASP.NET control.

    The code looks like this:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.IO;
    using System.Xml;
    using System.Text.RegularExpressions;
    using System.Reflection;
    
    namespace ControlReferenceGenerator
    {
        class Program
        {
            static void Main(string[] args)
            {
                new Generator().Generate();
            }
    
            public class Generator
            {
                Dictionary<string, string> mTemplates = new Dictionary<string, string>();
                Dictionary<string, string> mReplaced = new Dictionary<string, string>();
                Dictionary<string, string> mStringsToReplace = new Dictionary<string, string>();
                Dictionary<string, string> mFilePairs = new Dictionary<string, string>();
                Dictionary<string, string> mFileMatches = new Dictionary<string, string>();
                Dictionary<string, string> mTagPrefixes = new Dictionary<string, string>();
                List<string> mStringsToRemove = new List<string>();
                List<string> mDuplicates = new List<string>();
                private StringBuilder mOutput = new StringBuilder();
                string mTemplateDir = "";
                string mBaseDir = "";
    
                public void Generate()
                {
                    string exePath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
                    string templatesPath = Path.Combine(exePath, "templates.txt");
                    string debugPath = Path.Combine(exePath, "debug.txt");
                    string stringsToRemovePath = Path.Combine(exePath, "StringsToRemove.txt");
                    string stringsToReplacePath = Path.Combine(exePath, "StringsToReplace.txt");
                    string outputPath = Path.Combine(exePath, "output.txt");
                    try
                    {
                        string templateFileName = Console.ReadLine();
                        string path = "";
                        bool pathFound = false;
                        bool duplicatesFound = false;
                        string dir = Path.Combine(exePath, "..\\..\\..\\WebSite1");
                        DirectoryInfo baseDirectory = new DirectoryInfo(dir);
                        mBaseDir = baseDirectory.FullName;
                        FileInfo[] fis = baseDirectory.GetFiles("*.ascx", SearchOption.AllDirectories);
                        foreach (FileInfo fi in fis)
                        {
                            if (!mFileMatches.ContainsKey(fi.Name))
                            {
                                mFileMatches.Add(fi.Name, fi.FullName);
                                mFilePairs.Add(fi.FullName, "");
                            }
                            if (templateFileName.ToUpper() == fi.Name.ToUpper())
                            {
                                mDuplicates.Add(fi.FullName);
                                if (mDuplicates.Count > 1)
                                {
                                    duplicatesFound = true;
                                }
                            }
                            if (templateFileName.ToUpper() == fi.Name.ToUpper() && !pathFound)
                            {
                                path = fi.FullName;
                                pathFound = true;
                            }
                        }
                        if (duplicatesFound)
                        {
                            bool choiceEntered = false;
                            while (!choiceEntered)
                            {
                                int counter = 0;
                                Console.WriteLine("");
                                Console.WriteLine("Several files with the same name where found, please specify which one you want to process");
                                foreach (string duplicate in mDuplicates)
                                {
                                    Console.WriteLine("{0}: {1}", counter, duplicate);
                                    counter++;
                                }
                                string input = Console.ReadLine();
                                try
                                {
                                    int index = int.Parse(input);
                                    if (mDuplicates.Count <= index)
                                    {
                                        throw new Exception();
                                    }
                                    path = mDuplicates[index];
                                    choiceEntered = true;
                                }
                                catch { }
                                Console.WriteLine("Please provide valid number");
                            }
                        }
                        if (!File.Exists(path))
                        {
                            throw new Exception("File not exists");
                        }
                        mTemplateDir = Path.GetDirectoryName(path);
                        fis = new DirectoryInfo(dir).GetFiles("*.ascx.cs", SearchOption.AllDirectories);
                        foreach (FileInfo fi in fis)
                        {
                            string key = Path.GetFileNameWithoutExtension(fi.Name);
                            if (mFileMatches.ContainsKey(key))
                            {
                                mFilePairs[mFileMatches[key]] = fi.FullName;
                            }
                        }
                        using (StreamReader sr = File.OpenText(templatesPath))
                        {
                            while (!sr.EndOfStream)
                            {
                                string line = sr.ReadLine();
                                string[] arr = line.Split("-".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                                if (arr.Length == 2)
                                {
                                    string replaced = arr[0].Replace(":", "_").Trim();
                                    if (!mReplaced.ContainsKey(arr[0].Trim()))
                                    {
                                        mReplaced.Add(arr[0].Trim(), replaced);
                                    }
                                    if (!mTemplates.ContainsKey(replaced))
                                    {
                                        mTemplates.Add(replaced, arr[1].Trim());
                                    }
                                }
                                else if (arr.Length == 1)
                                {
                                    string replaced = arr[0].Replace(":", "_").Trim();
                                    if (!mReplaced.ContainsKey(arr[0].Trim()))
                                    {
                                        mReplaced.Add(arr[0].Trim(), replaced);
                                    }
                                    string type = arr[0].Replace("asp:", "").Replace("sa:", "").Trim();
                                    if (!mTemplates.ContainsKey(replaced))
                                    {
                                        mTemplates.Add(replaced, type);
                                    }
                                }
                            }
                        }
                        using (StreamReader sr = File.OpenText(stringsToReplacePath))
                        {
                            while (!sr.EndOfStream)
                            {
                                string line = sr.ReadLine();
                                string[] arr = line.Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                                if (arr.Length == 2)
                                {
                                    if (!mStringsToReplace.ContainsKey(arr[0].Trim()))
                                    {
                                        mStringsToReplace.Add(arr[0].Trim(), arr[1].Trim());
                                    }
                                }
                            }
                        }
                        using (StreamReader sr = File.OpenText(stringsToRemovePath))
                        {
                            while (!sr.EndOfStream)
                            {
                                string line = sr.ReadLine();
                                if (!mStringsToRemove.Contains(line.Trim()))
                                {
                                    mStringsToRemove.Add(line.Trim());
                                }
                            }
                        }
                        StringBuilder sb = new StringBuilder();
                        using (StreamReader sr = File.OpenText(path))
                        {
                            while (!sr.EndOfStream)
                            {
                                string line = sr.ReadLine();
                                if (line.Trim() != "")
                                {
                                    foreach (string key in mReplaced.Keys)
                                    {
                                        line = line.Replace(key, mReplaced[key]);
                                    }
                                    foreach (string key in mStringsToReplace.Keys)
                                    {
                                        line = Regex.Replace(line, key, mStringsToReplace[key]);
                                    }
                                    foreach (string key in mStringsToRemove)
                                    {
                                        line = line.Trim().Replace(key, "");
                                    }
                                    sb.AppendLine(line);
                                }
                            }
                        }
                        string str = sb.ToString();
                        Regex rgx = new Regex(@"<%[^%]*%>");
                        str = String.Concat("<div>", rgx.Replace(str, ""), "</div>");
                        using (StreamWriter sw = File.CreateText(debugPath))
                        {
                            sw.Write(str);
                        }
                        // Uncomment the next line to see the generated XML
                        System.Diagnostics.Process.Start(debugPath);
                        XmlDocument doc = new XmlDocument();
                        using (StringReader sr = new StringReader(str))
                        {
                            doc.Load(sr);
                        }
                        ParseNextLevel(doc.DocumentElement, "");
    
                        using (StreamWriter sw = File.CreateText(outputPath))
                        {
                            sw.Write(mOutput.ToString());
                        }
                        System.Diagnostics.Process.Start(outputPath);
                    }
                    catch (Exception ex)
                    {
                        using (StreamWriter sw = File.CreateText(outputPath))
                        {
                            sw.Write(ex.Message);
                        }
                        System.Diagnostics.Process.Start(outputPath);
                    }
                }
    
                private void ParseNextLevel(XmlElement element, string lastParent)
                {
                    if (element.Name == "Register")
                    {
                        string src = "";
                        string tagname = "";
                        string tagprefix = "";
                        foreach (XmlAttribute attr in element.Attributes)
                        {
                            if (attr.Name.ToLower() == "src")
                            {
                                src = attr.Value;
                            }
                            else if (attr.Name.ToLower() == "tagname")
                            {
                                tagname = attr.Value;
                            }
                            else if (attr.Name.ToLower() == "tagprefix")
                            {
                                tagprefix = attr.Value;
                            }
                        }
                        if (
                            !String.IsNullOrEmpty(src) &&
                            !String.IsNullOrEmpty(tagname) &&
                            !String.IsNullOrEmpty(tagprefix)
                            )
                        {
                            string key = String.Concat(tagprefix, "_", tagname);
                            //ASP.modules_servicing_usercontrols_ucassetinfo_ascx
                            string value = "";
                            DirectoryInfo di = new DirectoryInfo(mTemplateDir);
                            string[] arr = src.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                            foreach (string str in arr)
                            {
                                if (str == "..")
                                {
                                    di = di.Parent;
                                }
                                else if (str.EndsWith(".ascx"))
                                {
                                    value = str.Replace(".", "_").ToLower();
                                }
                                else
                                {
                                    di = new DirectoryInfo(Path.Combine(di.FullName, str));
                                }
                            }
                            while (di.FullName.ToLower() != mBaseDir.ToLower())
                            {
                                value = String.Concat(di.Name.ToLower(), "_", value);
                                di = di.Parent;
                            }
                            value = String.Concat("ASP.", value);
                            if (!mTagPrefixes.ContainsKey(key))
                            {
                                mTagPrefixes.Add(key, value);
                            }
                        }
                    }
                    if (mTagPrefixes.ContainsKey(element.Name))
                    {
                        GenerateOneReference(element, lastParent, mTagPrefixes[element.Name]);
                        lastParent = GetId(element);
                    }
                    if (mTemplates.ContainsKey(element.Name))
                    {
                        GenerateOneReference(element, lastParent, "");
                        lastParent = GetId(element);
                    }
                    foreach (XmlNode childElement in element.ChildNodes)
                    {
                        if (childElement is XmlElement)
                        {
                            ParseNextLevel((XmlElement)childElement, lastParent);
                        }
                    }
                }
    
                private string GetId(XmlElement element)
                {
                    foreach (XmlAttribute attr in element.Attributes)
                    {
                        if (attr.Name.ToLower() == "id")
                        {
                            return attr.Value;
                        }
                    }
                    throw new Exception(String.Format("Cannot find 'id' attribute for element: {0}", element.Name));
                }
    
                private void GenerateOneReference(XmlElement element, string lastParent, string type)
                {
                    if (lastParent != "")
                    {
                        if (type == "") type = mTemplates[element.Name];
                        mOutput.AppendLine(String.Format(@"
    private {0} {1}
    {{get{{return {2}.FindControl(""{1}"") as {0};}}}}",
                        type,
                        GetId(element),
                        lastParent
                        ));
                    }
                }
            }
        }
    }

     

     

     

     

    The only line you need to change to make it work in your environment is:

    string dir = Path.Combine(exePath, "..\\..\\..\\WebSite1");

    If the Console App is inside the same solution as WebSite you are working on then just change this path to relative path of the bin/debug directory of that console app to the website in the solution. This way the ReferenceGeneraton application can be placed in the source control and work fine on all developers' machines who currently works on the solution.

     

    One more note about the code :) Please don't judge me for some "dirtyness" of the code. It works really fine on a big controls and was written in a 2 hours.

     

    The whole source code can be downloaded here.

    Hope this helps.

    Monday, May 12, 2008

    Loading and Executing JavaScript Files From JavaScript, ASP.NET AJAX

    Download source code with samples for the article here.

    Quite often we need to load js files dynamically right from javascript. With ASP.NET AJAX it is simple.

    ASP.NET AJAX library has internal ScriptLoader class that can be used to load js files, specify callbacks that will be invoked when script is downloaded and ready, and execute functions inside newly loaded files.

    I will not list all available methods from ScriptLoader class - you can see them in more details in AJAX source.

    I will demonstrate how to load files and execute some functions in it.

    ScriptLoader has static and instance members. To add script references(urls) and script blocks to the loader we have to create instance of the class:

    image

    To add one or more script references to the loader use queueScriptReference method:

    image

    the parameter is path to the script file.

    And finally to load the file and insert link element to the body of the document use the following function:

    image

     

    Actually the loadScripts expects more parameters, but only first is required. The declaration of loadScripts function in AJAX library looks like this:

    image

    As you see the function can accept three callback functions that will be called when some condition is true: load was successful, load failed, or load was timed out.

     

    Whole sample will look like:

    image

    Test.js file contains the following code:

    image

    First line simply executes alert function.

    Second line is very important - this is how ScriptLoader knows when script is loaded to the end and calls success callBack.

    If you don't add this line to the end of the file ScriptLoader will never know is or is not script executed (loaded) to the end.

    Lets run the page:

     image

    Script was loaded and executed!

     

    Now lets try another example, where we will pass callback to the function to be able to call function inside dynamically loaded file, and be sure that it is loaded before we call anything in it.

    The page will look like:

    image

    and js file:

    image

    As you see we are calling the function inside file in callback, this way we ensure it is called AFTER file was loaded.

    Lets execute the page and first try to click second button:

    image

    Alert button says the script is not loaded yet - that's true. Now after we load the script (clicking on first button):

    image

    Is works.

     

    You may be wondering why you need this. But suppose you are loading contents to the browser dynamically, for example using web service or page method call, and the content is using javascript. You can also load dynamically it's script file and execute anything you need there.

     

    Hope this helps.

     

    Technorati Tags:

     

    kick it on DotNetKicks.com