Nuno Gomes

ASP.NET Health Monitoring - Building an EventLogWebEventProvider - Part 3

On my previous posts on this health monitoring series I explain to you how and why I made my own EventLogWebEventProvider and which benefits you can achieve by using this provider or by making your own.

Now I'll write about how to use this new provider in one application.

Well, almost everything have been written about this topic and Microsoft has one good article on "How To: Use Health Monitoring in ASP.NET 2.0" so I wont even try to explain all the possible scenarios and configurations, I will simply explain the standard scenario.

Typically I simply want to keep track on two Web Event types:

  1. the ones related to application errors
  2. an the others related to application life cycle

As far as I notice there are many people that simply track those from point 1.

Another decision to make is where to store these Web Events data, You do this by choosing a Web Event provider.

ASP.NET give us out-of-the-box several providers and you can make your own provider. Once your provider is done you can use it just like all the others.

In this example I will use the newly created provider.

Now that I know exactly what to track and where to store it I can update my configuration.

All health monitoring configuration data is stored in the system.web\healthMonitoring section and my standard configuration will look like:

<healthMonitoring enabled="true">
  <providers>
    <add name="ExtendedEventLogWebEventProvider" type="NG.Web.Management.EventLogWebEventProvider, NG.Web" source="MyEvtLogSource" />
  </providers>
  <rules>
    <clear />
    <add name="Application Lifetime Events Default" eventName="Application Lifetime Events" provider="ExtendedEventLogWebEventProvider" profile="Default" />
    <add name="All Errors Default" eventName="All Errors" provider="ExtendedEventLogWebEventProvider" profile="Default" />
  </rules>
</healthMonitoring>

Notice the source attribute from the ExtendedEventLogWebEventProvider, using it you can set which EventLog source to use.

Remember that when you create a EventLog source you can choose to create a brand new EventLog and if you choose so, all entries written by this provider in this application context will be isolated from all the others providing one easy way for visual tracking and filtering.

With the health monitoring data in place you should now be able to see the entries appearing in EventLog.

ASP.NET Health Monitoring - Building an EventLogWebEventProvider - Part 2

In the first post of this series I've manage to find the correct eventId for each Web Event type, and by this time the major problem has been solved, but I cannot yet write a correct entry into the EventLog.

I still have to decided the best severity type and category to apply.

Severity

If you look at entries in the EventLog generated by the default EventLogWebEventProvider you will find that they are marked mainly as Information and a few of them are also marked as Warning but no one is ever marked as Error.

Since I'm making my own provider I will take this chance to map the EventLog entry severity type according to the source Web Event.

Web Event

EventLog entry Severity
WebBaseEvent EventLogEntryType.Information
WebManagementEvent EventLogEntryType.Information
WebApplicationLifetimeEvent EventLogEntryType.Information
WebRequestEvent EventLogEntryType.Information
WebHeartbeatEvent EventLogEntryType.Information
WebBaseErrorEvent EventLogEntryType.Warning
WebRequestErrorEvent EventLogEntryType.Error
WebErrorEvent EventLogEntryType.Error
WebAuditEvent EventLogEntryType.Information
WebSuccessAuditEvent EventLogEntryType.SuccessAudit
WebAuthenticationSuccessAuditEvent EventLogEntryType.SuccessAudit
WebFailureAuditEvent EventLogEntryType.FailureAudit
WebAuthenticationFailureAuditEvent EventLogEntryType.FailureAudit
WebViewStateFailureAuditEvent EventLogEntryType.FailureAudit

Naturally, you can decided to map the Web Events exactly has the default EventLogWebEventProvider does.

Finally all I need is to do is set the correct category for the Web Events.

Category

Although all Web Events belong to the same category 'Web Events', the problem is how to make the correct text appear.

The value shown in the category property is a resource string and to select the correct category value is necessary to set the  exact resourcekey.

That is not so hard to do, I could have made my own resource assembly with only one resource, but I have a better way ... I will use the same resource assembly that ASP.NET use.

To figure out which assembly to use I simply used regedit.exe to look at "HK_LM\System\CurrentControlSet\Services\EventLog\Application\ASP.NET 2.0.50727.0"

registry_EventLog

What I look for are the settings used by the "ASP.NET 2.0.50727.0" EventLog source which is the source used to write the Web Events EventLog entries.

The settings I will use are the CategoryCount and CategoryMessageFile keys because they are the ones that instruct which category assembly to load.

Name

Data
CategoryCount 5
CategoryMessageFile C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_rc.dll

Please note that the CategoryMessageFile may change according to operating system and ASP.NET version.

Finally, I need to know the exact resourcekey of the "Web Event" string, and since there simply 5 resources in assembly it took no time to get the following table:

Category Resource Key

Category
0 None
1 Setup
2 Uninstall
3 Web Event
4 File Monitoring
5 Admin Service

Now I simply need to configure my EventLog source to use those same settings.

As I said in part 1, you can create an EventLog source using the EventLog.CreateEventSource method, but for setting the CategoryCount and CategoryMessageFile values you must use this specific overload EventLog.CreateEventSource(EventSourceCreationData sourceData).

That's it ... I've got everything I need and the final result look like this:

EventLog

It looks similar to the usual but if you look closer you will find the differences.

Benefits

What you need to keep in mind is that:

  • the EventId and Category values are keep unchanged;
  • the entry Type respects the Web Event type as opposing to the EventLogWebEventProvider given by ASP.NET that only uses Information and Warning;
  • the EventLog source can be one of my choise (not the usual "ASP.NET x.xxx") and can change by application;
  • you can choose either to use the existing EventLogs or to create a brand new one for your application(s) .

As a final note remember that with this provider you can now organize your application EventLog entries into specific EventLogs, and you may also filter them by application Source.

Download the code here.

Posted: Aug 24 2008, 10:51 PM by nmgomes | with 4 comment(s)
Filed under:
SQL Server - Undocumented Stored Procedure sp_MSforeachtable

I'm not an every day SQL Server user but I use SQL Server regularly since 7.0 version until the 2005 version (not yet tried 2008 in a serious way) and from time to time I still find some nice hidden gems.

A few days ago I needed to created a cleanup script for an application and one of the tasks was to drop all tables that match a specific name pattern.

My first thought was to use a cursor to loop or a dynamic SQL statement ...

... but this time I decided to google for some other approach, and I found the amazing undocumented sp_MSforeachtable stored procedure from the master database.

It does the same but it requires considerably less code and improves the script readability.

Below is the syntax for calling the sp_MSforeachtable SP: [more]

exec @RETURN_VALUE=sp_MSforeachtable @command1, @replacechar, @command2, 
  @command3, @whereand, @precommand, @postcommand

Where:

  • @RETURN_VALUE - is the return value which will be set by "sp_MSforeachtable"
  • @command1 - is the first command to be executed by "sp_MSforeachtable" and is defined as a nvarchar(2000)
  • @replacechar - is a character in the command string that will be replaced with the table name being processed (default replacechar is a "?")
  • @command2 and @command3 are two additional commands that can be run for each table, where @command2 runs after @command1, and @command3 will be run after @command2
  • @whereand - this parameter can be used to add additional constraints to help identify the rows in the sysobjects table that will be selected, this parameter is also a nvarchar(2000)
  • @precommand - is a nvarchar(2000) parameter that specifies a command to be run prior to processing any table
  • @postcommand - is also a nvarchar(2000) field used to identify a command to be run after all commands have been processed against all tables

As you can see, this stored procedure offer us some flexibility, but for the most common uses you will only use one or two of them.

Back to my problem, drop all tables with a specific naming pattern, I ended up using a script just like this:

declare @appName varchar(128)
declare @mycommand varchar(128)
declare @mywhereand varchar(128)

set @appName = 'xpto'

set @mycommand = 'drop table ?'
set @mywhereand = 'and o.name like ''' + @appName + '__Log__%'' escape ''_''

print 'Dropping all tables belonging to ' + @appName + ' application ...'

exec sp_MSforeachtable 
                @command1 = @mycommand, 
                @whereand = @mywhereand

What I'm saying here is that the command 'drop table' should be executed for every table that match the criteria name like 'xpto_Log_%'.

As you can see its fairly simple and clean and this is just the top of the iceberg.

For more detail about sp_MSforeachtable go here and here.

Posted: Aug 19 2008, 12:39 AM by nmgomes | with 1 comment(s)
Filed under:
ASP.NET - Health Monitoring and EventLogWebEventProvider - Part 1

The ASP.NET health monitoring enables you to add instrumentation to Web applications by using the so called Web Events. These Web events give us information about health status.

You can configure health monitoring by setting events and providers in the healthMonitoring section.

Naturally, ASP.NET provide us with a few out-of-the-box providers such as the EventLogWebEventProvider.

As many of you may have already notice, when using the EventLogWebEventProvider the events are added to the Application EventLog with the following source pattern:

        ASP.NET "framework version"

If you are using ASP.NET 2.0 the source will look similar to "ASP.NET 2.0.50727.0 ".

You can imagine what happen when a server hosts several web applications ... you can't easily figure which application raised a web event because you can't apply a filter to do that. To figure it out you must inspect the eventlog entry data.

If you think this is wrong and that Microsoft should do something about it, go here and here, vote and comment.

What can you do to overlap this? Well you can create your own EventLogWebEventProvider that allows you to specified which Source to use.

Doing such a provider is fairly simple but lead us to THE problem: which eventId to use when creating the EventLog entry?

What? Why is this a problem? are you saying.

Well, lets start all from the beginning ... you want to create your own provider so you can specify the EventLog source but you certainly desire to keep the remaining settings unchanged so that monitoring applications that track the EventLog entry for well known eventIds still working fine.

The problem is that Microsoft don't expose the algorithm used to created the eventId from the WebEvent data, and this way we can only guess which eventId to use.

If you look at EventLogWebEventProvider.ProcessEvent method you will find the following code:

int num = UnsafeNativeMethods.RaiseEventlogEvent((int) type, (string[]) dataFields.ToArray(typeof(string)), dataFields.Count); 

This is your black box, no source or information is available.

To guess which eventId is used for a specific Web Event I created a small page that raises all known Web  Events.

I found that even with all known Web Events configured to use EventLogWebEventProvider almost half of them don't appear in EventLog, but those that have an EventLog entry made me speculate that eventIds are sequential and follow the classes hierarchy. Here are the results:

Web Event EventLog entry eventId Speculated eventId range
WebBaseEvent - 1303
WebManagementEvent - 1304
WebApplicationLifetimeEvent 1305 1305
WebRequestEvent 1306 1306
WebHeartbeatEvent 1307 1307
WebBaseErrorEvent - 1308
WebRequestErrorEvent 1309 1309
WebErrorEvent 1310 1310
WebAuditEvent - 1311
WebSuccessAuditEvent 1312 1312
WebAuthenticationSuccessAuditEvent - 1313
WebFailureAuditEvent 1314 1314
WebAuthenticationFailureAuditEvent - 1315
WebViewStateFailureAuditEvent 1316 1316

 

Please note that I'm considering that no two different Web Events share the same eventId.

If you believe the assumptions made are correct you can now start coding your provider.

Remember that you must create the EventLog source before  use it. You can do this by using the EventLog.CreateEventSource method.

ASP.NET - EventMappingSettingsCollection bug on Contains method

Recently, while digging on ASP.NET 2.0 Health Monitoring I found a bug in the EventMappingSettingsCollection.Contains class method.

I was trying to check if an event mapping already exists but every time I try it the following exception was thrown:

System.NullReferenceException was unhandled by user code
  Message="Object reference not set to an instance of an object."
  Source="System.Web"
  StackTrace:
       at System.Web.Configuration.EventMappingSettingsCollection.GetElementKey(ConfigurationElement element)
       at System.Configuration.ConfigurationElementCollection.GetElementKeyInternal(ConfigurationElement element)
       at System.Configuration.ConfigurationElementCollection.BaseIndexOf(ConfigurationElement element)
       at System.Web.Configuration.EventMappingSettingsCollection.IndexOf(String name)
       at System.Web.Configuration.EventMappingSettingsCollection.Contains(String name)

I opened a bug in connect, so, if you think this is important go there and vote.

And if you think this one is not a common error ... well ... this is the second bug I found regarding Contains method from a collection class.

NunoGomesControlToolkit - Improving Web Apps performance

A few weeks ago I told you about a control toolkit I was making.

I decided to call him NunoGomesControlToolkit and is intended to improve web apps performance by decreasing total page size. This page size reduction is achieved by decreasing control ClientID size.

This control toolkit can be applied to any existing ASP.NET 2.0 Web Aplication by using the tagmapping configuration facility.

To obtain maximum redution it's also recommended to extend webforms, masterpages and usercontrols not from regular Page, MasterPage and UserControl controls from ASP.NET framework but instead use the corresponding control from NunoGomesControlToolkit.

TagMapping is only used for markup interpretation and therefore all dynamic created controls are not mapped. To override this limitation its also included in this toolkit the DynamicControlBuilder class. Use this class to allow tagmapping over dynamic created controls.

As I promise, the control toolkit is now available at code.msdn.microsoft.com.

I'm currently applying the NunoGomesControlToolkit to the BlogEngine.NET 1.3 version, and as soon as I test it I will make it available.

kick it on DotNetKicks.com

ASP.NET Controls - Improving automatic ID generation : The ShortIDs Naming Provider (Part 4)

In the previous posts on this subject I wrote about why automatic ID generation should be improved and how we can improve it. Now I will step forward and show you my own implementation of a specific naming provider.

As we saw in part 3, to create a specific Naming provider you only need to develop your own implementation of SetControlID method.

I named my naming provider ShortIDsProvider and it will have only one specification to meet:

  • it will create IDs in the form Txxx

where T denotes the 'T' character and xxx denotes an unique incremental integer value.

Why using the 'T' character? Well, since Control.ID must always start by an alphabetic character ... I choose on my own risk the 'T' char.

So ... here it is:

public class ShortIDsProvider : NamingProvider
{
    #region Private Fields
    private const string ID_PREFIX = "T";
    private static object KeepLongIDsAttributeValueKey = new object();
    #endregion Private Fields

    #region Public Methods
    private bool KeepOriginalID(System.Web.UI.Control control)
    {
        bool keepOriginalIDs = false;

        #region KeepLongIDs Attribute Value Management
        if (!this.KeepOriginalIDs)
        {
            if (HttpContext.Current.Items.Contains(KeepLongIDsAttributeValueKey))
            {
                keepOriginalIDs = (bool)HttpContext.Current.Items[KeepLongIDsAttributeValueKey];
            }
            else
            {
                string path = System.Web.HttpContext.Current.Request.Path.Replace(System.Web.HttpContext.Current.Request.ApplicationPath, string.Empty);

                if (this.ExceptionList != null && this.ExceptionList.Contains(path))
                {
                    keepOriginalIDs = true; ;
                }
                else
                {
                    if (control.Page != null)
                    {
                        object[] atts = control.Page.GetType().GetCustomAttributes(true);

                        foreach (Attribute att in atts)
                        {
                            if (att is KeepOriginalIDsAttribute)
                            {
                                keepOriginalIDs = ((KeepOriginalIDsAttribute)att).Enabled;
                                break;
                            }
                        }
                    }
                }
                HttpContext.Current.Items[KeepLongIDsAttributeValueKey] = keepOriginalIDs;
            }
        }
        #endregion KeepLongIDs Attribute Value Management

        return keepOriginalIDs;
    }
    #endregion Public Methods

    #region NamingProvider Implementation

    /// <summary>
    /// Generates the Control ID.
    /// </summary>
    /// <param name="name">The controls name.</param>
    /// <param name="control">The control.</param>
    /// <returns></returns>
    public override string SetControlID(string name, System.Web.UI.Control control)
    {
        if (this.KeepOriginalID(control))
        {
            return name;
        }
        if (control == null)
        {
            throw new ArgumentNullException("control");
        }
        if (control.NamingContainer == null)
        {
            return name;
        }
        NamingContainerControlCollection controlsCollection = control.NamingContainer.Controls as NamingContainerControlCollection;
        if (controlsCollection == null)
        {
            return name;
        }

        string shortid = null;
        if (!controlsCollection.ContainsName(name))
        {
            shortid = string.Format("{0}{1}", ID_PREFIX, controlsCollection.GetUniqueControlSufix());

            if (string.IsNullOrEmpty(name))
            {
                name = shortid;
            }
            controlsCollection.RegisterControl(shortid, name, control);
        }
        else
        {
            shortid = control.ID;
        }
        return shortid;
    }

    #endregion NamingProvider Implementation

}

As you can see it's not rocket science, and enable us to create any automatic id generation strategy.

Naturally this ShortIDsProvider is only valuable in conjugation with a set of improved web controls.

Making an improved web control is also very simple and straight forward. Here is the improved TextBox control:

public class TextBox : System.Web.UI.WebControls.TextBox
{
    #region Naming Management

    /// <summary>
    /// Creates a new <see cref="T:System.Web.UI.ControlCollection"></see> object to hold the child controls (both literal and server) of the server control.
    /// </summary>
    /// <returns>
    /// A <see cref="T:System.Web.UI.ControlCollection"></see> object to contain the current server control's child server controls.
    /// </returns>
    protected override ControlCollection CreateControlCollection()
    {
        return NamingConfiguration.Provider.CreateControlCollection(this);
    }

    /// <summary>
    /// Gets or sets the programmatic identifier assigned to the server control.
    /// </summary>
    /// <value></value>
    /// <returns>The programmatic identifier assigned to the control.</returns>
    public override string ID
    {
        get{ return NamingConfiguration.Provider.GetControlID(this, base.ID); }
        set { base.ID = NamingConfiguration.Provider.SetControlID(value, this); }
    }

    /// <summary>
    /// Searches the current naming container for a server control with the specified id and an integer, specified in the pathOffset parameter, which aids in the search. You should not override this version of the <see cref="Overload:System.Web.UI.Control.FindControl"></see> method.
    /// </summary>
    /// <param name="id">The identifier for the control to be found.</param>
    /// <param name="pathOffset">The number of controls up the page control hierarchy needed to reach a naming container.</param>
    /// <returns>
    /// The specified control, or null if the specified control does not exist.
    /// </returns>
    protected override Control FindControl(string id, int pathOffset)
    {
        Control ctrl = base.FindControl(id, pathOffset);
        if (ctrl == null)
        {
            ctrl = NamingConfiguration.Provider.FindControl(this, id, pathOffset);
        }
        return ctrl;
    }

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"></see> event.
    /// </summary>
    /// <param name="e">An <see cref="T:System.EventArgs"></see> object that contains the event data.</param>
    protected override void OnInit(EventArgs e)
    {
        this.EnsureID();
        this.ID = base.ID;
        base.OnInit(e);
    }
    #endregion Naming Management

}

The last step is configuration: first to instruct asp.net to use our improved web controls instead of the usual ASP.NET controls. We do this thru TagMapping configuration, like this:

<system.web>
  <pages>
    <tagMapping>
      <add tagType="System.Web.UI.WebControls.TextBox, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
           mappedTagType="IDsSample.Controls.TextBox"/>
    </tagMapping>
  </pages>
</system.web>

And second, setting the default Naming provider. The web.config should look similar to this one:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="NamingConvention" type="IDsSample.Configuration.NamingSection"
          allowDefinition="MachineToApplication"
          restartOnExternalChanges="true" />

  </configSections>
  <appSettings/>
  <connectionStrings/>
  <system.web>
    <compilation debug="true"/>
    <authentication mode="Windows"/>

    <pages>
      <tagMapping>
        <add tagType="System.Web.UI.WebControls.TextBox, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             mappedTagType="IDsSample.Controls.TextBox"/>
      </tagMapping>
    </pages>
  </system.web>

  <NamingConvention defaultProvider="ShortIDs">
    <providers>
      <add name="ShortIDs" type="IDsSample.Providers.ShortIDsProvider"
           exceptionlist="/Exception.aspx"
           keeporiginalids="false"/>
    </providers>
  </NamingConvention>

</configuration>

Please note that TagMapping will only work for controls declared in markup. If you need to create dynamic controls then use the DynamicControlBuilder class.

I'm currently working on a SiteKit that can be applied to any existing web application in order to increase performance by reducing the rendered html size.

While waiting for the SiteKit you can try the sample solution.

kick it on DotNetKicks.com

Posted: Jun 19 2008, 12:40 AM by nmgomes | with 1 comment(s)
Filed under:
ASP.NET Controls - Improving automatic ID generation : Architectural Changes ( Part 3)

Naming container controls are a subclass of standard controls, that differ in the ability to manage child controls' ID, in fact, these naming container controls are the key to unique ID generation. To become a namingcontainer a regular control must implement the INamingContainer interface.

In order to override ASP.NET ID generation we will have to work in two fronts:

  • override regular controls' behavior to decouple the Control.UniqueID property from the Control.ID property
  • override naming container controls to allow us to control how ID generation is done and how to find a control 

To decouple the Control.UniqueID property from the Control.ID property without changing the ASP.NET paradigm I chose to set the Control.ID property base value with a computed value and map it to the original ID value. Naturally, this computed value is composed by its Control.NamingContainer.

Since settings the value of Control.ID has changed, getting its value must change to act accordingly.

public override string ID
{
    get { return NamingConfiguration.Provider.GetControlID(this, base.ID); }
    set { base.ID = NamingConfiguration.Provider.SetControlID(value, this); }
}

As you can notice, there's a provider working here, but the key that makes this work is:

protected override void OnInit(EventArgs e)
{
    this.EnsureID();
    this.ID = base.ID;
    base.OnInit(e);
}

This little code is used to ensure the correct initialization of ID mapping, i.e, that the initial value of Control.ID is generated from value set on markup, if present.

In the naming container control I want to change the way child controls' IDs are managed, so I decided to create a new ControlCollection that will aggregate all child control ID management logic.

protected override ControlCollection CreateControlCollection()
{
    return NamingConfiguration.Provider.CreateControlCollection(this);
}

Naturally, if the ControlCollection changes, the FindControl method needs to change too.

protected override Control FindControl(string id, int pathOffset)
{
    Control ctrl = base.FindControl(id, pathOffset);
    if (ctrl == null)
    {
        ctrl = NamingConfiguration.Provider.FindControl(this, id, pathOffset);
    }
    return ctrl;
}

What we can see here is that FindControl method can find controls either by computed Id (for internal purposes) or by control name (for human purposes).

These are the only members that  any control needs to override. The implementation details are left to the NamingProvider and NamingContainerControlCollection classes.

The NamingProvider

The NamingProvider class has methods, one of those methods is abstract and its goal is to compose/generate an automatic Id.

public abstract string SetControlID(string name, System.Web.UI.Control control);

The other methods are:

/// <summary>
/// Creates a controls collection.
/// </summary>
/// <param name="control">The owner control.</param>
/// <returns></returns>
public ControlCollection CreateControlCollection(System.Web.UI.Control control)
{
    return new NamingContainerControlCollection(control);
}

/// <summary>
/// Gets the control name given the control's id.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="id">The controls id</param>
/// <returns></returns>
public string GetControlID(System.Web.UI.Control control, string id)
{
    if (control == null)
    {
        throw new ArgumentNullException("control");
    }
    if (control.NamingContainer != null)
    {
        NamingContainerControlCollection namingContainerCollection = control.NamingContainer.Controls as NamingContainerControlCollection;
        if (namingContainerCollection != null)
        {
            return namingContainerCollection.GetName(id);
        }
    }
    return id;
}

/// <summary>
/// Finds a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="id">The id.</param>
/// <param name="pathOffset">The path offset.</param>
/// <returns></returns>
public Control FindControl(System.Web.UI.Control control, string id, int pathOffset)
{
    if (control == null)
    {
        throw new ArgumentNullException("control");
    }

    NamingContainerControlCollection controlsCollection = control.Controls as NamingContainerControlCollection;
    if (controlsCollection == null)
    {
        return null;
    }

    return controlsCollection.FindControl(id, pathOffset);
}

This provider model also allows us to control when to keep original IDs by setting the following attributes in provider configuration:

  • keeporiginalids - specifies whether to keep original IDs
  • exceptionlist - set a list of pages that will allways render the original IDs ( comma separated )

Now, the only core entity that's missing is NamingContainerControlCollection.

The NamingContainerControlCollection

The NamingContainerControlCollection class extends the  ControlCollection class and manages child controls' names and IDs. This is done by using two collections:

  • Dictionary<string, string> m_linkDictionary - to provide the link between the Id and Name;
  • Dictionary<string, System.Web.UI.Control> m_nameDictionary - to provide a collection of controls by name.

The following methods are also added:

/// <summary>
/// Determines whether a control is in the <see cref="NamingContainerControlCollection"></see>.
/// </summary>
/// <param name="name">The controls name.</param>
/// <returns>
///     <c>true</c> if a control is found; otherwise, <c>false</c>.
/// </returns>
public bool ContainsName(string name)
{
    if (string.IsNullOrEmpty(name))
    {
        return false;
    }
    return m_nameDictionary.ContainsKey(name);
}

/// <summary>
/// Gets the control name given the control id.
/// </summary>
/// <param name="id">The control's name.</param>
/// <returns></returns>
public string GetName(string id)
{
    if (string.IsNullOrEmpty(id))
    {
        return id;
    }
    if (ContainsID(id))
    {
        string name = m_linkDictionary[id];
        //if (baseid == shortid)
        //{
        //    return null;
        //}
        return name;
    }
    return id;
}

/// <summary>
/// Registers the pair [name, control] and link id to name.
/// </summary>
/// <param name="id">The id value.</param>
/// <param name="name">The name value.</param>
/// <param name="control">The control.</param>
public void RegisterControl(string id, string name, Control control)
{
    m_nameDictionary.Add(name, control);
    m_linkDictionary.Add(id, name);
}

With all this changes in place and ready to be used we only need to create a specific NamingProvider, in fact, we only need to implement the GenerateAutomaticID method, but that ... will came soon.

kick it on DotNetKicks.com

Posted: Jun 04 2008, 12:51 AM by nmgomes | with 4 comment(s)
Filed under:
Less MSIèrables - Extract the content of a .MSI file

Yesterday, I decided to spent some time reviewing the WindowsLiveWriter plugin gallery and search for a replacer for Steve Dunn CodeFormatter plugin. I've been using this plugin since the beginning of the year but it reveal some problems composing the Html.

My friend Paulo told me about Douglas Stockwell "Paste from Visual Studio" plugin and I decided to try this one.

I download the vspaste.msi and tried to install it without success. As far as I understood (I didn't take this to depth) the .MSI was looking for a specific registry entry and since I'm using the portable version of WLW the key wasn't found.

I knew that WLW plugins don't required any special install procedure, they just need to be in the plugins folder.

I decided then to extract the .MSI content in order to be able to copy the assembly to the correct folder in my flash drive.

Searching the web I found a little tool called "Less MSIèrables" that exactly feets my needs.

It's a free tool written in C# for .NET 1.1 that allow us to open an .MSI file, explore its contents, and extract selected items.

lessmsi

We can also run it from the command line.

Its not perfect and I found a few possible improvements:

  • GUI - adding Drag'n'Drop capability
  • GUI - adding a select/unselect all files checkbox
  • CPI - adding a -l option to list all files within a .MSI file
  • CPI - adding a -e option to extract a list of files

The good news is that author made the source code available, so anyone can extend the tool.

In conclusion, it's free, it's cool and I think I going use it many times in future, helping me to see if I really need to install a .MSI.

For those of you that prefer not to use free tools, Microsoft provides us with msiexec.exe tool. This one is available in Windows XP and higher and also allow us to extract .MSI files using the following syntax:

msiexec.exe /a vspaste.msi /qb TARGETDIR=c:\temp

Posted: May 29 2008, 12:34 AM by nmgomes | with no comments
Filed under:
ASP.NET - Dynamic Control Mapping

I already posted here about Tag Mapping and how helpful it can be, but naturally there's are a few improvements that I would like to see available in future framework release.

The one I expect the most is about the capability of mapping dynamic created controls using the same rules as Tag Mapping uses when interpreting the page markup.

Without this capability we can never use widely the tag mapping because whenever we need to create dynamic controls they will be strongly coupled to a specific control implementation.

Imagine this scenario:

  1. First you have built an web application that use standard ASP.NET TextBox  control, some of them dynamically created.
  2. Now, imagine that you want to reuse that application, as is, but instead of ASP.NET Textbox control you want to use your own Textbox implementation.

This task could be easily accomplished using Tag Mapping if no dynamic controls were used, but in this scenario ASP.NET give us no solution, so the application cannot be reused without modifications.

Naturally, you can copy/paste your application and make the necessary changes, or even add a few if statements, but that will only increase complexity and maintenance effort.

Until the .NET team provide us such capability we must do the magic ourselves.

My proposal is an help class (DynamicControlBuilder) that provide us two methods: GetMappedType and CreateControl.

/// <summary> /// Gets the mapped <see cref="System.Web.UI.Control"/> type. /// </summary> /// <param name="type">The <see cref="System.Web.UI.Control"/> type to be mapped</param> /// <param name="prefix">The namespace prefix.</param> /// <returns>A <see cref="System.Type"/> object.</returns> public static Type GetMappedType(Type type, string prefix) { if (!typeof(Control).IsAssignableFrom(type)) { throw new ArgumentOutOfRangeException("type", "Must inherit from Control."); } Type mappedtype; if (!string.IsNullOrEmpty(prefix)) { TagPrefixInfo prefixinfo; if (!m_prefixes.TryGetValue(prefix, out prefixinfo)) { throw new ArgumentException("prefix", "No prefix found."); } else { type = BuildManager.GetType(string.Format("{0}.{1}, {2}", prefixinfo.Namespace, type.UnderlyingSystemType.Name, prefixinfo.Assembly), false); if (type == null) { throw new ArgumentException("type", "Control not found within specified prefix."); } } } if (m_tagMappings.TryGetValue(type.UnderlyingSystemType, out mappedtype)) { return mappedtype; } return type; } /// <summary> /// Creates a dynamic mapped <see cref="System.Web.UI.Control"/>. /// </summary> /// <param name="type">The <see cref="System.Web.UI.Control"/> type to be mapped</param> /// <param name="prefix">The namespace prefix.</param> /// <returns>A <paramref name="T"/> object.</returns> public static Control CreateControl(Type type, string prefix) { Type mappedType = GetMappedType(type, prefix); ; return (Control)Activator.CreateInstance(mappedType); }

The main goal is to enable any of the following usages:

this.Page.Controls.Add(DynamicControlBuilder.CreateControl<System.Web.UI.WebControls.TextBox>("foo")); this.Page.Controls.Add(DynamicControlBuilder.CreateControl(typeof(System.Web.UI.WebControls.TextBox), "foo")); this.Page.Controls.Add(DynamicControlBuilder.CreateControl(typeof(System.Web.UI.WebControls.TextBox)));

Try it !

DynamicControlBuilder.cs (7.13 kb)

kick it on DotNetKicks.com

More Posts Next page »