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.