Tag Archives: Winforms

ListViewEx – Auto resize selected column

What do you do when you get fed up having to write the same code over and over again? Write a base class for it… of course.

I regularly use the WinForms ListView control and often want to specify a specific column (in detail view) that must auto resize when the whole control resize. Therefore I created the following class that does all that (and more stuff) to make life easier.

public class ListViewEx : ListView
    {
        public ListViewEx()
            : base()
        {
            DoubleBuffered = true;
            View = View.Details;
            resizeTimer.Tick +=  resizeTimer_Tick;
        }

        [Description("Use one column to auto resize in detail view")]
        public bool AutoResizeColumnEnabled { get; set; }
        [Description("Column index of auto resize column")]
        public int AutoResizeColumnIndex { get; set; }
        public event MethodInvoker EnterKeyPressed;
        public event MethodInvoker DeleteKeyPressed;
        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            if (e.KeyChar == '\r')
                if (EnterKeyPressed != null)
                {
                    EnterKeyPressed();
                    e.Handled = true;
                }
            base.OnKeyPress(e);
        }
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Delete)
                if (DeleteKeyPressed != null)
                {
                    DeleteKeyPressed();
                    e.Handled = true;
                }
            base.OnKeyDown(e);
        }
        private Timer resizeTimer = new Timer() { Interval = 100, Enabled = false };
        private void resizeTimer_Tick(object sender, EventArgs e)
        {
            resizeTimer.Enabled = false;
            try
            {
                if (AutoResizeColumnEnabled && View == System.Windows.Forms.View.Details &&
                    AutoResizeColumnIndex > -1 && this.Columns.Count > AutoResizeColumnIndex)
                {
                    int columnsWidth = 0;
                    Application.DoEvents();
                    for (int i = 0; i < this.Columns.Count; i++)
                    {
                        if (i != AutoResizeColumnIndex)
                            columnsWidth += this.Columns[i].Width;
                    }
                    this.Columns[AutoResizeColumnIndex].Width = this.ClientSize.Width - columnsWidth - 2;
                }
            }
            catch { }
        }
        protected override void OnResize(EventArgs e)
        {
            resizeTimer.Enabled = false;
            resizeTimer.Enabled = true;
            base.OnResize(e);
        }
    }

Now all you have to do is set the AutoResizeColumnEnabled property to true and specify a column index for AutoResizeColumnIndex and there you go. Easy hey?

VS 2013 Express tips

Just thought to share a tip or two about making the user experience better. The following applies to the desktop version but should also work (with some minor adjustment) to the other editions as well.

Change the all UPPERCASE main menus

In the registry, find the key HKEY_CURRENT_USER\Software\Microsoft\WDExpress\12.0\General. Add a (32-bit) DWORD value named SuppressUppercaseConversion and give it a value of 1. Next time you open the IDE the menu’s will be normal Title case again. It also applies to VS2012 editions as well (just change the version number to 11.0). For web express just change the ‘WDExpress’ part.

Update: If you are using the Web express edition the registry path for the change upper case menus are: HKEY_CURRENT_USER\Software\Microsoft\VWDExpress\12.0\General

Adding more themes

By default there are only 3 themes (for the express versions). To add some more options go to http://alinconstantin.blogspot.com/2012/09/using-color-themes-with-visual-studio.html?_sm_au_=isV7sFqHt8JbtbNM

Adding my Source backup tool

My source backup tool id aware of the desktop express version and can automatically add itself as one of the custom tools of the IDE. See SourceBackup.

If I come across more tips I’ll add them here as well.

 

Windows Forms vs WPF

The following short piece is about my opinion of comparing old fashioned Windows Forms development to using WPF (Windows Presentation Foundation).

After some real experimenting, researching, googling, scratching, digging, hair pulling (what is left of it), swearing, yelling, kicking around getting a very simple WPF application to work I can seriously say that WPF is a load of … brown stuff. I’m not denying that it could be used to create really beautiful and powerful applications given enough time, patience (and hair) but, the effort to do this is making me question whether this is worth while. It has a few benefits over WinForms but they are offset by some really annoying and lacking features, not to mention that it also have all the same disadvantages that WinForm applications have (compare to stuff like Web/mobile applications). Essentially, anything you can do with WPF can also be done with WinForms with less time and effort.

Let me give a few examples.

Basic layout and design of a form

I’m sure once you are completely fluent with WPF and all the possible permutations of properties, behavior, styles etc. that you can also do basic layouts of an application very quickly. For me, knowing exactly what I wanted and even what I’m looking for, it took more that two days to create a basic interface (which I could have done in 5 minutes in WinForms). Yes, part of the problem was I’m not fluent in all that is WPF but thanks to Google I could find solution quickly every time. Despite this I had to retest every possible basic behavior of all controls – even simple things like clicking on a blank space inside a control to ensure all ‘basic’ functionality as a plain user would expect (people that have been using Windows app for years), are still the same. One of the most important fundamental concepts of UI design is to have consistency – like behavior of controls. Something like keyboard navigation was seriously not part of the WPF designers ideas.

The ListView control.

This control has been changed in several ways which could both be seen and good and bad. In WinForms this is/was one of my favorites. The way you design, code and use columns are very different from WinForm ListViews. It does give you more powerful ‘formatting’ options but this make it incredibly complex as well. Doing basic stuff like right aligning a column is not straight forward anymore. Yes, it can be done but this requires doing ‘non-standard’ things like ‘Styles’, custom code and so on. Then behavior like selecting items in the ListView has changed and even something like ‘unselecting’ all items by clicking on a blank space inside the control now requires you to write ‘extra’ code just to accomplish it. Essentially this is not the same ListView control that we were used to in WinForms but a new control that superficially looks like it.

There is however one aspect/feature of this control that I really like. The way you can bind columns to properties of an object – which by the way is the only way to use it! In general I do NOT use databinding features because it removes your ability to do really powerful things with the control and more importantly databinding controls never really work well with huge sets of data. For that reason I always populated my controls myself so I have ‘real’ control over them. That does come at the cost of having to manually always sync the displayed values on a control with the underlying data.

Summary

There might be people out there that prefer WPF over an ‘old’ technology like WinForms and possible they can create even better applications if they put their mind to it but I’m still in the WinForms camp. Then you have to keep in mind that WPF just like WinForms as a development technology is in danger of being abandoned thanks to the whole HTML5 and JavaScript mobile revolution.  WPF might become just another one of those potential (good) technologies that Microsoft killed before it had time to evolved into something really good. That is a shame.

Update:

I also discovered that WPF does not even support some VERY basic dialog boxes like browsing for a folder. I mean, FFS Microsoft, what were the developers of WPF smoking when they created this abortion of a tool/technology??

Also, they changed the very standard way of returning results from dialog/messages boxes to use a nullable boolean in stead of a usable structure. WTF??

Parallel threading and Control.BeginUpdate or EndUpdate

Yesterday I learned the hard (old) way that using BeginUpdate and EndUpdate in a multithreading environment (Windows Forms) don’t work so ‘lekker’ together. In hindsight, I should have guessed that this would be a problem but 20/20 hindvision really doesn’t help after the fact.

While making enhancements to my QuickMon tool to facilitate multithreading (using the .Net 4 parallel extensions) while calling collectors I started experiencing weird and unpredictable freezes or application hangs – app seems to be hanging but some parts of the UI still updates but the window cannot be moved, closed (in the normal way) etc. I tried various ways using mutexes, Control.Invokes, extra timers and duck tape to try and solve the problems. Only once I removed all my BeginUpdate/EndUpdates did the freezing issue disappear.

As mentioned before it all made sense afterwards. The problem is that when multiple background threads make calls back to the UI thread (yes, even using the proper Control.Invoke() method) that has a BeginUpdate in the beginning and later an EndUpdate there is no guarantee that every BeginUpdate is followed by its EndUpdate. That could mean some EndUpdates might never be reached properly.

The solution (for now) was to just disable all BeginUpdate/EndUpdates. Of course, a more proper design change would be to use some kind of buffer and a separate update UI routine that function outside the callbacks coming from the multiple threads collecting data. This unfortunately is not a small change and cannot be done in just a day or two. In a future iteration I’ll try to implement something like this for the QuickMon Windows client. The Windows service version of the tool wasn’t affected and work as is with the multi-threading change.

TreeViewEx – a few enhancements

Sometimes I wonder why some functionality that should be common is simply left out of general controls.. like the WinForms TreeView (and ListView) controls that don’t have specific events for the enter key pressed. I’ve been taught long ago to always provide functionality so keyboard events also can be used to navigate around applications – like when the mouse is unavailable, broken or not your first choice (like for disabled people). Yes, I know the controls have general events for key pressing but it requires some hectic coding each time to do something that should be simple.

Anyway, adding it isn’t too difficult (but I still wished I didn’t have to do it each time I need to use it).

public class TreeViewEx : TreeView
{

public TreeViewEx() : base()
{

DoubleBuffered = true;

}
public event MethodInvoker EnterKeyPressed;
public event MethodInvoker DeleteKeyPressed;
protected override void OnKeyPress(KeyPressEventArgs e)
{

if (e.KeyChar == ‘\r’)

if (EnterKeyPressed != null)
{

EnterKeyPressed();
e.Handled = true;

}

base.OnKeyPress(e);

}

protected override void OnKeyDown(KeyEventArgs e)
{

if (e.KeyCode == Keys.Delete)

if (DeleteKeyPressed != null)
{

DeleteKeyPressed();
e.Handled = true;

}

base.OnKeyDown(e);

}

}

The code for the ListView control is basically (exactly) the same.

Extending the TreeView control

Here is a little Extension class to the TreeView control. I created it to add some new functionality that is not available in the base control. It is based on the System.Windows.Forms namespace TreeView but probably can be use relatively easy without too much modification on WPF as well (haven’t tried it yet)

public static class TreeViewEx
    {
        public static int CheckedCount(this TreeView tvw)
        {
            int count = 0;
            count = (from TreeNode n in tvw.Nodes
                     where n.Checked
                     select n).Count();
            foreach (TreeNode childNode in tvw.Nodes)
            {
                count += CheckedCount(childNode);
            }

            return count;
        }
        public static int CheckedCount(this TreeNode parentNode)
        {
            int count = 0;
            count = (from TreeNode in parentNode.Nodes
                     where n.Checked
                     select n).Count();
            foreach (TreeNode childNode in parentNode.Nodes)
            {
                count += CheckedCount(childNode);
            }
            return count;
        }
        public static List<TreeNode> CheckedNodes(this TreeView tvw)
        {
            List<TreeNode> checkedNodes = new List<TreeNode>();
            foreach(TreeNode node in (from TreeNode in tvw.Nodes
                         select n))
            {
                if (node.Checked)
                    checkedNodes.Add(node);
                checkedNodes.AddRange(node.CheckedNodes().ToArray());
            }
            return checkedNodes;
        }
        public static List<TreeNode> CheckedNodes(this TreeNode parentNode)
        {
            List<TreeNode> checkedNodes = new List<TreeNode>();
            foreach (TreeNode node in (from TreeNode in parentNode.Nodes
                                           select n))
            {
                if (node.Checked)
                    checkedNodes.Add(node);
                checkedNodes.AddRange(node.CheckedNodes().ToArray());
            }
            return checkedNodes;
        }
        public static void ExpandAllParents(this TreeNode node)
        {
            TreeNode parent = node.Parent;
            while (parent != null)
            {
                parent.Expand();
                parent = parent.Parent;
            }
        }
    }

Embedding a Form inside another control

This is something I was thinking about at the beginning of my TDI (Tabbed document interface) ‘adventure’ but I didn’t think it would be easy or possible back then. Then I came across a custom tab control (A highly configurable MDI tab control from scratch) that actually allowed you to use plain old WinForms to add to it. The control itself was written in VB and ultimately I abandoned using it but I was intrigued with how it could use a plain WinForm. It actually has some real advantages like.

  • A form you can design easier in the IDE (than a user control).
  • A form can be tested/used separately on it own without being embedded onto something else first.
  • And I had a blank thinking of more reasons…

If you try to just add a Form to another container – like a panel, tabpage etc. and try to run it you would get the error: ‘Top-level control cannot be added to a control.‘. This happens because Forms are marked as Top Level controls. Fortunately, there is a simple way around it. You need to mark the form as not being a ‘Top level control’ like this: (assuming you have a panel called panel1)

Form f = new Form();
f.TopLevel = false;
panel1.Controls.Add(f);
f.Show(); //this is needed to actually have the form display itself inside the container

That will make the form load inside the panel without run-time errors – but it would look ugly if you just leave it like that. To properly ’embed’ it you need to add some more code:

Form f = new Form();
f.TopLevel = false;
f.MdiParent = null;
f.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
f.Dock = DockStyle.Fill;
panel1.Controls.Add(f);
f.Show();

So, there you have it!

Form2 f = new Form2();
            f.TopLevel = false;

Tab control

I’ve come across a tab control that I can customize for my needs in creating a MDI window replacement. It is based on the KRBTabControl article from Karub Yaz?l?m on CodeProject.

I’ve added some built-in context menu that adds a few options for closing a tab, closing all tabs, closing all other tabs and view a list of the available tabs from a modal dialog window. The rest is pretty much the original control except for some enums that I moved around.

My version can be found here.

* Update *

I’ve added an event to the control to fire when a tab page get removed. The code to achieve this looks like this:

public event TabPageRemovedDelegate TabPageRemoved;
private void RaiseTabPageRemoved(TabPageEx tab)
{

if (TabPageRemoved != null)

TabPageRemoved(tab);

}

The ‘RaiseTabPageRemoved’ method then gets called from the main OnMouseClick plus my new context menu events to close tabs.

Multilingual applications

Living in the good old S of A it is natural for us to be bilingual or even multilingual. So what do we do if we have to create software that can/must be used by people of multiple language backgrounds? Traditionally Microsoft had the concept of resource files that you can use to create multiple versions of the same application for different languages or locales. Problem with this is once the executable has been compiled it is also fixed. What happens if you want the application to be multilingual at ‘run-time’?

Well, I’ve had to do something like this for myself. I first looked at resource files but it just did not solve the whole issue on its own. So I started creating something for myself that works for my application. Part of the solution still use .Net resource files for storing xml data that gets use in the language look up procedure. The idea is to load the text of controls and windows at the time the window open since control and window text get loaded at run-time anyway. The following is an explanation of the classes I created to facilitate my solution. It is a fully working solution and already in use inside a proper application. There may be other and even better or smarter solutions ‘out there’ but this one works for me.

LanguageHandler

This is the central class that handles all the language setting/reading etc. functionality. It encapsulate the list of ‘phrases’ used in the application and provides methods to retrieve words or phrases depending on a look-up key. Simply translating words alone is not good enough so the focus is on whole ‘phrases’.

The following is a partial view of the class without methods:

public static class LanguageHandler
{

private static List<Phrase> phraseList = new List<Phrase>();

#region Properties
private static string languageID = “en-za”;
public static Language AppLanguage {

get {

switch (languageID) {
case “af”:
return Language.Afrikaans;
case “en-za”:
return Language.English;
default:
return Language.Afrikaans; }

}
set {

switch (value) {
case Language.Afrikaans:
languageID = “af”;
break;
case Language.English:
languageID = “en-za”;
break;
default:
languageID = “en-za”;
break; }

}

public static string PhraseSource { set; get; }
#endregion

}

PhraseList.xml

The definition of the phrases can probably be stored in any data form but I choose a plain simple xml file to host the raw phrase data. This make it easy to maintain and port. It is also included into the application as a file resource although theoretically it could be located anywhere as long as the application can reach it.

The structure is also very basic as it is simply a serialized version of List<Phrase>

<?xml version=”1.0″ encoding=”utf-16″?>
<ArrayOfPhrase xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<phrase id=”someId” default=”Some default”>

<phraseEntry lang=”af”>Afrikaanse waarde</phraseEntry>
<phraseEntry lang=”en-za”>English value</phraseEntry>

</phrase>
<phrase id=”someId2″ default=”Some default 2″>

</ArrayOfPhrase>

LoadPhrases method

Loading the data is as simple as deserializing the data. I have a helper class (not shown) to simply take the xml file and deserialize it:

#region LoadPhrases
public static void LoadPhrases()
{

if ((PhraseSource != null) && (PhraseSource.Length > 0))

phraseList = SerializationUtils.DeserializeXML<List<Phrase>>(PhraseSource);

}
#endregion

GetLanguagePhrase method

The main (and only relevant) method for retrieving stuff from the class is GetLanguagePhrase. It simply takes one parameter – the id value of the phrase required.

#region GetLanguagePhrase
public static string GetLanguagePhrase(string languagePhraseID)
{

if (phraseList.Count == 0)

LoadPhrases();

Phrase phrase = (from p in phraseList where p.LanguagePhraseID == languagePhraseID select p).FirstOrDefault();
if (phrase != null)
{

PhraseEntry phraseEntry = (from pe in phrase.Phrases where pe.LangId == languageID select pe).FirstOrDefault();
if (phraseEntry != null)

return phraseEntry.Text;

else

return phrase.DefaultValue;

}
throw new Exception(“Undefined language phrase ID”);

}
#endregion

SetControlTextAndTip methods

To make it a bit easier to set the text (and tooltips if available) of controls (my implementation use WinForms) I created a few helper methods to automatically set the text on the control based on a phrase id specified.

#region SetControlText
public static void SetControlTextAndTip(string languagePhraseID, params ToolStripItem[] tsis)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (ToolStripItem tsi in tsis)
{

tsi.Text = text;
tsi.ToolTipText = text;

}

}
public static void SetControlTextAndTip(string languagePhraseID, params Control[] crtls)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (Control crtl in crtls)
{

crtl.Text = text;

}

}
public static void SetControlTextAndTip(string languagePhraseID, params ColumnHeader[] columnHeaders)
{

string text = GetLanguagePhrase(languagePhraseID);
foreach (ColumnHeader ch in columnHeaders)
{

ch.Text = text;

}

}
#endregion

The Phrase and PhraseEntry classes

These two classes are really simple. For the purpose of this article they could be a lot simpler but since I built a separate ‘editor’ they have a bunch of attributes to make editing easier. This editor plus a few method inside LanguageHandler will have to wait for another article.

[Serializable, XmlType(“phrase”)]
public class Phrase : IComparable
{

[XmlAttribute(“id”),
Browsable(true),
CategoryAttribute(“Identifier”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Phrase ID”),
ReadOnly(true)]
public string LanguagePhraseID { get; set; }
[XmlElement(“phraseEntry”),
Browsable(true),
CategoryAttribute(“Value details”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Phrase entries”)]
public List<PhraseEntry> Phrases { get; set; }
[XmlAttribute(“default”),
Browsable(true),
CategoryAttribute(“Value details”),
DefaultValueAttribute(“”),
Editor(typeof(MultilineStringEditor), typeof(UITypeEditor)),
DescriptionAttribute(“Default value. Use ‘\\r\\n’ to indicate crlf.”)]
public string DefaultValue { get; set; }

#region IComparable Members
public int CompareTo(object obj)
{

Phrase otherPhrase = (Phrase)obj;
return LanguagePhraseID.CompareTo(otherPhrase.LanguagePhraseID);

}
#endregion

}

[Serializable, XmlType(“entry”), TypeConverter(typeof(PhraseEntryConverter))]
public class PhraseEntry
{

public PhraseEntry() { }
public PhraseEntry(string phraseEntryDef)
{

if (phraseEntryDef.Contains(“:”))
{

LangId = phraseEntryDef.Substring(0, phraseEntryDef.IndexOf(“:”));
Text = phraseEntryDef.Substring(phraseEntryDef.IndexOf(“:”) + 1);

}

}

[XmlAttribute(“lang”),
Browsable(true),
CategoryAttribute(“Phrase entry”),
DefaultValueAttribute(“”),
DescriptionAttribute(“Language”)]
public string LangId { get; set; }
[XmlText(),
Browsable(true),
CategoryAttribute(“Phrase entry”),
DefaultValueAttribute(“”),
Editor(typeof(MultilineStringEditor), typeof(UITypeEditor)),
DescriptionAttribute(“Text”)]
public string Text { get; set; }

public override string ToString()
{

return LangId + “:” + Text;

}

}

Using it all

Using LanguageHandler is really easy. Typically in a form’s onload event I call a private local method that sets all the texts of the controls that need to be set. There is no real performance issue to worry about since all the phrase data is stored in memory. One disadvantage is that normally the form need to be closed and reopened if the ‘language’ has change. This is not really a problem since changing the language is something that should not happen often. Technically this private LoadLanguageResources method could be ran any number of times whenever you want. The only place where I usually call it multiple times is on the ‘Options’ dialog where you choose the language being used.

If you have to use the same phrase multiple times you can always store the value in a local string variable.

The following is a short example of how to use it:

static class Program
{

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{


if (Properties.Settings.Default.LanguageID == “English”)

LanguageHandler.AppLanguage = Language.English;

else

LanguageHandler.AppLanguage = Language.Afrikaans;

LanguageHandler.PhraseSource = Properties.Resources.PhraseList;
LanguageHandler.LoadPhrases();

Form mainForm = new MainForm();

Application.Run(mainForm);

}

public partial class MainForm : Form
{

….

private void LoadLanguageResources()
{

Text = LanguageHandler.GetLanguagePhrase(“AppName”);
LanguageHandler.SetControlTextAndTip(“File”, fileToolStripMenuItem);
LanguageHandler.SetControlTextAndTip(“NewPerson”, newPersoonToolStripMenuItem);

}

private void MainForm_Load(object sender, EventArgs e)
{

LoadLanguageResources();

}

}

Summary

As you can see the solution is not really complicated. I really like simple solutions that ‘just works’. This one has proven to be working for my needs so far. I have over 340 different phrases, some are very long themselves like message box prompts.

HTMLWriter 1.2

And with another iteration the library has improved yet again. See the previous or original posts for details about where it came from.

With this version I’ve made some big changes internally to the library. You can still use the basic functionality as with version 1.0 but the auto indentation has been improved greatly – and simplified in the code. Essentially it now properly formats the generated html with indentation for tags specified to do so.

To make it much easier to manage auto formatting tags I modified the AppendTagStart and AppendTagEnd methods to make use of some string arrays that define tags that require standard behavior – like always insert a crlf in-front of it or always append crlf etc.

The arrays looks like this:

startTagsAutoOnNewLine = new string[]
{

“address”,
“blockquote”,
“code”,
“div”,
“h1″,”h2″,”h3″,”h4″,”h5″,”h6”,
“iframe”,
“ol”, “ul”, “dl”,
“table”, “thead”, “tfoot”, “tbody”, “tr”, “td”, “th”

};

tagsAutoIncDecIndentation = new string[]
{

“address”,
“blockquote”,
“ol”, “ul”, “dl”,
“table”, “thead”, “tfoot”, “tbody”, “tr”

};

endTagsAutoOnNewLine = new string[]
{

“address”,
“blockquote”,
“ol”, “ul”, “dl”,
“table”, “thead”, “tfoot”, “tbody”, “tr”,

};

endTagsAutoAppendCRLF = new string[]
{

“address”,
“blockquote”,
“code”,
“h1″,”h2″,”h3″,”h4″,”h5″,”h6”,
“iframe”,
“ol”, “ul”, “dl”,
“table”

};

The 2 methods look like this:

protected void AppendTagStartInternal(string tagName, string className, params CustomAttribute[] customAttributes)
{

if (AutoFormatting)
{

if (startTagsAutoOnNewLine.Contains(tagName.ToLower()))
{

AppendNewLineInternal();
AppendIndentation();

}
else if (lastWrittenCRLF)
{

AppendIndentation();

}

}
this.AppendInternal(string.Format(“<{0}”, tagName));
if (className.Length > 0)

this.AppendInternal(string.Format(” class=\”{0}\””, className));

foreach (CustomAttribute customAttribute in customAttributes)
{

this.AppendInternal(” ” + customAttribute.ToString());

}
this.AppendInternal(“>”);
tags.Push(tagName);
if (AutoFormatting)
{

if (tagsAutoIncDecIndentation.Contains(tagName.ToLower()))
{

IndentationInc();
AppendNewLineInternal();

}

}

}

protected void AppendTagEndInternal(string tagName, bool autoPop)
{

if (autoPop && tags.Peek() == tagName)

tags.Pop();

if (AutoFormatting)
{

if (tagsAutoIncDecIndentation.Contains(tagName.ToLower()))
{

IndentationDec();

}
if (endTagsAutoOnNewLine.Contains(tagName.ToLower()))
{

AppendNewLineInternal(); ;
AppendIndentation();

}
else if (lastWrittenCRLF)
{

AppendIndentation();

}

}

this.AppendInternal(“</” + tagName + “>”);

if (AutoFormatting)
{

if (endTagsAutoAppendCRLF.Contains(tagName.ToLower()))
{

AppendNewLineInternal();

}

}

}

Additionally I split the HTMLWriter class into 2 – the plain HTMLWriter and a base class – HTMLWriterBase. The reason is simple – it makes it easier to maintain core/internal functionality separately but also allow for another custom implementation of another type of xyzWriter.

The DataTable specific methods were also enhanced to allow for a hyperlink (anchor tag) to be embedded based on one of the fields inside the DataTable. It allows you to specify an editing page, the parameter name passed to the page, the linked id field from the DataTable and the display field in which the link will be placed.

Available here.