Channels ▼
RSS

.NET

Moving To .NET 2.0

Source Code Accompanies This Article. Download It Now.


August, 2005: Moving to .NET 2.0

Eric has developed everything from data reduction software for particle bombardment experiments to software for travel agencies. He can be contacted at ericterrell@ comcast.net.


Visual Studio 2005, .NET 2.0, and C# 2.0 include a host of new features. But since your .NET 1.1 app probably runs as-is on .NET 2.0, is there any rush to load it into Visual Studio 2005 and start exploiting new .NET and C# functionality? I recently ported one of my .NET 1.1 applications to 2.0 to learn about the promise and perils of .NET 2.0—and I learned a lot more than I expected! During the port, I discovered bugs in my application that I never knew I had. I found that some programming techniques that worked flawlessly in .NET 1.1 were either partially or completely nonfunctional in .NET 2.0. And I learned which C# and Windows Forms enhancements were useful to me, and which were not. In this article, I show how you convert .NET 1.1 applications to 2.0 while taking advantage of the most compelling features in this new platform. I've included the C# Programmable Calculator, an application that I ported (available electronically; see "Resource Center," page 3) to .NET 2.0. This article and the sample application are based on the Beta 1 versions of Visual Studio 2005 and .NET 2.0. A few of the details may change as Visual Studio and the .NET platform evolve.

The Sample App

The C# Programmable Calculator (Figure 1) is a Reverse Polish Notation (RPN) or HP-style calculator. RPN calculators have no parenthesis, no operator precedence rules, and no "=" button. You enter the operands first, then select the operation. For example, to calculate "2+2," press the 2 button, then press Enter. Press 2 again and press +. When you press +, the operands are removed from the stack and replaced with the result of the operation. The stack is displayed as a list box in the upper left.

Users can add new buttons to the calculator by writing C# methods. Press the Edit Functions button to add a new custom button. Scroll until the cursor is inside the Functions class and isn't in the middle of a method. Select Edit/Add Function and choose the button's tab, as well as the function name and return type. After you press the OK button, write the method's code. Then press OK and go to the tab you specified. You'll see a new button corresponding to the method you just added. The code you were editing was dynamically compiled into an in-memory assembly. Then this assembly was interrogated by the Reflection API. The calculator's custom buttons were generated from the methods with [Button] attributes. When you press a custom button, the corresponding method in the assembly will be called, and the results will be placed on top of the stack.

There are two versions of the sample app. The Visual Studio 2003 and 2005 versions are stored in the "Dot Net 1.1" and "Dot Net 2.0" folders, respectively. To install either version, navigate to the Setup folder and double-click cspcalc.zip. If you use WinZip, you can automatically install by clicking the Install toolbar button. Otherwise, you may need to extract the .zip file to a hard disk folder and run setup.exe. To build either version, extract the source code to a hard disk folder and load the .sln file into Visual Studio. If you want to install the .NET 2.0 version, you'll need the .NET 2.0 framework. You'll also need Visual Studio 2005 or Visual C# 2.0 Express Edition to build the software and debug it.

Testing a .NET 1.1 App on .NET 2.0

Before I started the port to 2.0, I wanted to find out how well the original application ran on .NET 2.0. I had both .NET 1.1 and 2.0 installed on my machine but, by default, applications compiled for 1.1 only run in 2.0 if 1.1 is not available. There are at least two ways to force a 1.1 application to run on 2.0: Use a config file or a registry setting. Store a config file such as this in the folder containing your .exe:

<?xml version ="1.0"?>
<configuration>
<startup>
<requiredRuntime version="v2.0.40607"/>
<supportedRuntime version="v2.0.40607"/>
</startup>
</configuration>

Be sure that the config file's name is identical to your .exe's name, with an extra ".config" on the end. For example, the sample app's .exe is CSPCALC.exe, so the config file must be named CSPCALC.exe.config. The "v2.0.40607" version specifies .NET 2.0 Beta 1. If you're using a subsequent beta or the released version of 2.0, change the version attribute accordingly. Hint: You can find the installed .NET versions on your machine by looking at the folder names in your \%windir%\Microsoft.NET\Framework folder. If you have multiple 1.1 apps to test on 2.0, it takes effort to create config files for each application. In this case, use RegEdit to add a key named OnlyUseLatestCLR with a DWORD value of 1 in the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework section of the registry.

I configured the 1.1 version of C# Programmable Calculator to run on .NET 2.0 and ran it. To verify that it was really running on .NET 2.0, I wrote this custom function. The first argument of the Button attribute specifies the Tab that contains the button. The second argument is optional and specifies the button text. By default, the button text is the same as the method name.

[Button("Main", ".Net Version")]
public static Version DotNetVersion()
{
return Environment.Version;
}

Because this function returned 2.0.40607.42, I knew the app was running on .NET 2.0 Beta 1. I continued testing the 1.1 version of the app on .NET 2.0 and didn't find any significant bugs. After convincing myself that the program ran without problems on .NET 2.0, I updated its setup program. By default, a setup program created in Visual Studio 2003 will not install an app unless .NET 1.1 is installed on the user's system. The default value for the setup's SupportedRuntimes element is "8:1.1.4322," which is .NET 1.1's version number (except for the "8:" prefix). I added .NET 2.0 Beta 1's version number to SupportedRuntimes to allow the setup to install on systems with just 1.1, just 2.0 Beta 1, or both. You can change the SupportedRuntimes element as follows: Right-click the Setup project in the Solution Explorer. Select View/Launch Conditions and edit the .NET Framework value. Separate each version number with a semicolon ";".

Building and Testing With Visual Studio 2005

The next step was to compile and test the application with Visual Studio 2005. Visual Studio converted the .sln and .csproj files with no errors. The app built successfully, but when I ran it the first time, I immediately encountered threading errors when I called custom functions by pressing the corresponding buttons. In the Win32 environment, a GUI component must only be accessed and manipulated by the thread that created it. In most of my code, I followed this rule, but I broke it in a few places. Fortunately, the Visual Studio 2005 debugger detected these issues. I wrote a small test application named "ThreadTest" (Figure 2) that illustrates this situation. Load the code into Visual Studio 2003 and run it in the debugger.

When you click Update from the GUI Thread button, the current time will be updated (see updateFromGUIThreadButton_Click in Listing One). When you click Update from the Worker Thread button, a new worker thread is created, and this thread updates the time by calling UpdateTime to change the currentTimeLabel's text. However, this is a flagrant violation of Win32 threading rules. The only clue that something is wrong is that the Assert will fail. .NET GUI classes, such as Form and Control, have an InvokeRequired property that is True when a GUI object is accessed by the wrong thread. To correctly update the time from a worker thread, press Update from Worker Thread (Invoke). In this case, a new worker thread is created. When it's started, it calls the UpdateTimeFromGUIThread method. UpdateTimeFromGUIThread creates a delegate to UpdateTime, and calls UpdateTime indirectly by passing the delegate to the Form's Invoke method. This causes the UpdateTime method to be called by the GUI thread. I recommend testing all of your multithreaded applications in Visual Studio 2005. You may have hidden threading bugs that the new debugger can easily detect.

I noticed another issue as I ran the program with the Visual Studio 2005 debugger. The output window was constantly scrolling messages about first-chance exceptions. The exceptions were being caught, but I was concerned about the frequency with which they were occurring. After all, throwing and catching exceptions is computationally expensive. The exceptions were being thrown by the code that determines which buttons should be enabled, based on the number of items on the stack. For example, the + button is only enabled when there are at least two numbers on the stack. This code was throwing an exception if there were fewer than two items on the stack. Even though the exception was properly caught, this reduced performance, especially on slower machines. I restructured the method, StackListBox.StackItemsAreNumeric, to check the stack depth rather than throw an exception. Bravo, again, to the new debugger for bringing an important, previously unnoticed issue to my attention!

More Bugs

The new debugger uncovered other bugs. The app was accessing a browser window using COM interop. When the browser was launched from a worker thread, I got this error message: "Cannot instantiate ActiveX object because current thread is not in a single-threaded apartment." It's not often that an error message is so accurate. I fixed this issue by calling SetApartmentState(ApartmentState.STA) on the worker thread.

Calling Process.Start's one-argument overload with a URL string to launch a web browser didn't work in dynamically compiled code. I had to use the two-argument flavor, and specify the browser's .exe filename (for instance, "IExplore.exe" or "Firefox.exe") as the first argument. This issue does not affect ordinary code, just code in dynamically compiled assemblies. I found another bug in the Help/About dialog box. This form includes a hyperlink to my e-mail address. When clicked, it launches the user's e-mail client and automatically composes an e-mail with the addressee and subject line filled in. Code like this works on .NET 1.1:

Help.ShowHelp
(this, "mailto:EricTerrell@hotmail.com? subject= C# Programmable Calculator");

But in .NET 2.0, the e-mail address includes the ?subject= text inappropriately. Fortunately, this code works in both .NET versions:

Process.Start
("mailto:EricTerrell@hotmail.com?
subject= C# Programmable Calculator");

After fixing these bugs, I corrected some deprecation issues that the compiler detected. Then it was time to start exploiting the new C# language, Windows Forms, and .NET platform enhancements in .NET 2.0.

Nullable Value Types

In .NET 1.1, only objects can have null values. Floating-point variables can have a value of NaN ("not a number") that signifies "undefined" or "not applicable." But other value types do not have a special value for this purpose. In .NET 2.0, all value types can be nullable. To declare a variable of a nullable value type, just put a "?" at the end of the type name:

int? count1;
int? count2 = null;
int? count3 = 42;

Because C# 2.0 supports nullable types, and because my C# Programmable Calculator lets users write their own C# code for custom functions, I needed to test the app's ability to support nullable value types. I wrote these functions to see if the app would compile the code and handle the nullable values correctly:

[Button("Main", "Null Test 1")]
public static int? NullTest1()
{
return 42;
}
[Button("Main", "Null Test 2")]
public static int? NullTest2()
{
return null;
}

When I added NullTest1() and NullTest2(), the code compiled without errors. When I ran NullTest1(), it returned "42" as expected. But NullTest2() caused a blank item to be pushed on the stack (Figure 1). This occurred because the app was calling object.ToString() to display values in the stack. A nullable object's ToString() method will return an empty string when the object is null. Representing a nullable object null value as an empty string may be reasonable in many situations, but displaying a blank stack entry in a calculator will confuse users. I fixed this problem by checking for nullable object type values in StackListBox.StackListBox_DrawItem:

// If item is the empty string, could
// be a nullable type null value.
if (NullableTypeUtils.IsNullableType
(currentObject))
{
display = NullableTypeUtils.ToString
(currentObject);
}

The NullableTypeUtils.IsNullableType method determines if an object is a nullable value type. If so, NullableTypeUtils.ToString() converts the value to its string representation if it's nonnull (or "null" otherwise); see Listing Two. In addition to updating the list box display code, I updated the custom functions in the Statistics tab to ignore null value types.

Many of .NET 1.1's containers are object based. For example, the ArrayList container contains data of type object. It's the programmer's job to cast the contents of an ArrayList into objects of the correct class:

ArrayList arrayList = new ArrayList(2);
Class1 object1 = new Class1();
arrayList.Add(object1);
Class2 object2 = new Class2();
arrayList.Add(object2);
object1 = (Class1) arrayList[0];
object2 = (Class2) arrayList[1];

In .NET 1.1, the type safety of ArrayList and most other containers is enforced at runtime. If you cast an ArrayList item to the wrong type, you'll only realize it when you run the program. For example, if you change the aforementioned code to assign arrayList[1] to object1 and arrayList[0] to object2, you'll get an InvalidCastException. .NET 2.0 includes generic containers such as List<T> that enforce type safety at compile time. For example:

List<Class1> list = new List<Class1>(2);
Class1 object1 = new Class1();
Class1 object2 = new Class1();
list.Add(object1);
list.Add(object2);
object1 = list[0];
object2 = list[1];
// syntax error:
// string str = "";
// list.Add(str);
Class1[] array = list.ToArray();

.NET 2.0 generic containers cannot contain objects of fundamentally different classes. For example, if you uncomment the aforementioned commented-out lines, the compiler issues this syntax error: "Argument '1': cannot convert from 'string' to '...Class1'." If you want to store objects of multiple types in a generic container, make sure that all the objects are derived from the same base class and specify the base class in the container declaration. You could even declare a generic container to contain any object (List<object>), but then you'd defer type checking to runtime, just like in .NET 1.1. My productivity is much higher when type safety is verified automatically at compile time. That's why I converted most of the program's ArrayLists into List<T> containers.

In the .NET 1.1 version of the sample app, there are several utility classes that contain only static methods and static members. For example, see Version.cs in the .NET 1.1 source code. Since this utility class contains no nonstatic member variables and no nonstatic methods, there's no reason to ever instantiate an object of this class. Consequently, the class includes a private constructor to prevent developers from making this mistake. This class is marked as sealed because there's nothing to gain by inheriting from it. .NET 2.0 implements the utility class pattern with static classes. Static classes cannot contain constructors because there's no object to construct. Additionally, they are implicitly sealed. Like the utility class pattern, developers cannot instantiate objects of static classes. Better still, they cannot accidentally declare static class member variables or method parameters. I converted Version and the rest of my utility classes into static classes. See Version.cs in the .NET 2.0 source code.

In .NET 1.1, the entire code for each class must be in one source file. This can lead to large and messy source files in Windows Forms code because all the GUI settings (button names, control positions, and the like) are stored in the same file containing the code that you write. Visual Studio 2005 exploits partial classes to reduce clutter in Windows Forms source files. A partial class is a class whose code is spread across multiple files. The code that the developer adds to a Form goes into one source file, and the GUI settings go into another. Form1's code is spread across two files. Form1.cs (Listing Three) contains the developer code, Form1.Designer.cs (Listing Four contains the GUI settings. Another clutter reducer is the Control.GenerateMember property. If a control's GenerateMember property is False, a member variable in the Form class will not be created. For example, because label1's GenerateMember property is False, the Form class doesn't have a corresponding member, just a local variable near the top of the InitializeComponent method (see Listing Four). button1's GenerateMember property is True, hence the member variable near the bottom of Listing Four. I haven't used any partial classes in the sample app, nor have I exploited the Control.GenerateMember property. But I'll use both features the first time I add a new Form.

The sample app uses a web browser control to display loan amortization schedules when users press the Finance tab's Amortize button. The .NET 1.1 version of the program uses COM interop to display the data in an Internet Explorer window using the shdocvw.dll system DLL. This is easy enough to do, but Visual Studio generates two new assemblies, AxInterop.SHDocVw.dll and Interop.SHDocVw.dll, which must be installed with the application. These assemblies take up about 176 KB of space. .NET 2.0 includes a managed web browser component. Just select a WebBrowser control from the Toolbox and drop it on your form. WebBrowser components don't require the COM interop assemblies, so your application's code size won't increase by much. When I converted the sample app to use the managed WebBrowser, I found another advantage—it's easier to load HTML content. Just assign the HTML to the WebBrowser's DocumentText property. In the 1.1 version, I had to write the HTML text to a temporary file and then point the web browser to that file.

Start Your NGENs!

By default, .NET applications are converted from intermediate language (IL) code into the CPU's native instruction set by a just-in-time (JIT) compiler every time they're launched. The NGEN (native code generator) command-line utility, which has been available since .NET 1.1 lets you perform the JIT compile once. When NGEN is run on an application, the application's native code is generated and cached. When the app is run, there is no JIT overhead because the native code is loaded from the cache. In previous .NET versions, the NGEN utility was inconvenient to use because it required you to NGEN each assembly individually. The 1.1 version of the sample app includes an executable and eight DLLs, so NGEN must be run nine times. .NET 2.0's version of NGEN has a new install option that will automatically compile the application's .exe and all dependent assemblies in one call. You can find NGEN.exe in the %windir%\Microsoft.NET\Framework\v2.0.40607 folder. This path applies to .NET 2.0 Beta 1. The version number will change for subsequent betas and the released version. There's no guarantee that NGEN will improve a given application's performance. Before deciding to NGEN your application, I recommend that you measure the memory and performance impact. If you find that NGENing your app increases performance, you may want to update your setup program to automatically invoke NGEN's install option when the app is installed, and invoke NGEN's uninstall option when the app is removed.

Features I Didn't Use

Needless to say, I didn't find every new feature of Visual Studio 2005, C#, and .NET 2.0 to be relevant to my application. As a former LISP programmer, I was excited about C# 2.0's new anonymous method feature. If a method has a delegate parameter, you can specify the method's code inline in the call. For example, the Square method takes a delegate and an x parameter. It calls the delegate with the specified x value and returns the square of the result. In button1_Click, the first call to Square uses a named method argument. The second call uses an anonymous method argument with inline code. In both cases, Square computes (2x+3)2:

public delegate double
FunctionDelegate(double x);
// Evaluate the delegate with the specified // x value
// and return the square of the result.
double Square(FunctionDelegate f, double x)
{ return f(x) * f(x); }
// Named method
double TwoXPlus3(double x)
{ return 2.0 * x + 3; }
private void button1_Click(object sender, EventArgs e)
{
double result1 = Square(TwoXPlus3, 2.0);
double result2 = Square(delegate(double x)
{ return 2.0 * x + 3; }, 2.0);
MessageBox.Show(result1.ToString() + ",
" + result2.ToString());
}

I'll probably start using anonymous methods when I add functions to the calculator that evaluate functions passed in as delegate parameters. For example, when I add custom functions to compute a function's definite integral or find its roots, anonymous methods may come in handy.

Visual Studio 2005 includes a simple and powerful way to manage application configuration settings. To add settings, right-click on a Windows Forms project in the Solution Explorer. Select Properties and then select the Settings tab. Then add your settings to the grid. Each setting can have application or user scope. Application settings are read-only and apply to all users. User settings are read-write and apply to individual users. User variables are stored in the user's Application Settings folder. You can refer to configuration variables like this: Properties.Settings.Value.{variable name}. Call Properties.Settings.Value.Save to persist your configuration settings to disk. The configuration settings are automatically loaded when your app is launched, so there's no Properties.Settings.Value.Load method. I didn't use this new feature only because the application already has a mechanism to serialize and deserialize configuration settings (see GlobalConfig.SerializeConfiguration). Also, the Beta 1 implementation of this feature has a few bugs. I expect they will be fixed in Beta 2.

Finally, another .NET 2.0 feature that I wanted to use, but didn't, was the new MaskedTextBox. I was hoping to use MaskedTextBox controls to replace the edit controls that are used to enter binary, octal, decimal, and hexadecimal numbers in the Computer Math tab. For example, when you press the Enter Binary button, you're only allowed to enter 0 and 1 digits. I was hoping to replace my ad hoc code with a MaskedTextBox. Unfortunately, the MaskedTextBox doesn't have a mechanism to specify a list of legal digit characters. I think that MaskedTextBox should have allowed an optional regular expression to specify the legal inputs. I guess I'll have to create my own RegularExpressionTextBox control to fill this gap.

Conclusion

The enhancements in Visual Studio 2005, C# 2.0, and .NET 2.0 are evolutionary, not revolutionary. Nonetheless, they offer a host of useful features. I was pleased that my .NET 1.1 application ran on .NET 2.0 without significant issues. I was extremely impressed with the Visual Studio 2005 debugger's ability to detect threading bugs. This is hugely important now that multithreading is becoming essential to achieving optimal performance on today's CPUs. I was able to improve the app's performance because the debugger displays first-chance exceptions in the output window. Other compelling features include nullable value types, generic collections, partial classes, and the new managed WebBrowser component. Although it's premature to analyze the performance of .NET 2.0 this early in the beta phase, it's encouraging that NGEN has been enhanced to JIT-compile entire applications by automatically analyzing dependencies. .NET 2.0 and Visual Studio 2005 are expected to ship in the second half of 2005. If you're not already familiarizing yourself with the new platform, now's the time to get started.

DDJ



Listing One

// Update the current time display.
private void UpdateTime()
{
  // Check to see if this method is called from thread that created this Form.
  Debug.Assert(!InvokeRequired);
  currentTimeLabel.Text = DateTime.Now.ToLongTimeString() + 
                    " (" + Thread.CurrentThread.Name + ")";
}
private delegate void UpdateTimeDelegate();
// Update the current time display, but ensure that the update is performed 
// by the same thread that created this Form.
private void UpdateTimeFromGUIThread()
{
  UpdateTimeDelegate updateTimeDelegate = 
      new UpdateTimeDelegate(UpdateTime);
  Invoke(updateTimeDelegate);
}
// Update the time when the user presses the Update from GUI Thread button.
private void updateFromGUIThreadButton_Click(object sender,System.EventArgs e)
{
  UpdateTime();
}
// Update the time from a worker thread from a worker thread. This is 
// against the Win32 threading rules!
private void updateFromWorkerThreadButton_Click(
                      object sender, System.EventArgs e)
{
  Thread thread = new Thread(new ThreadStart(UpdateTime));
  thread.Name = "Worker Thread 1";
  thread.Start();
}
// Update the time from a worker thread, but use Invoke to ensure that the 
// update is done by the thread that created the Form.
private void updateFromWorkerThreadInvokeButton_Click(
                    object sender, System.EventArgs e)
{
  Thread thread = 
     new Thread(new ThreadStart(UpdateTimeFromGUIThread));
  thread.Name = "Worker Thread 2";
  thread.Start();
}
Back to article


Listing Two
internal static class NullableTypeUtils
{
  // Return true if the object is a nullable value type.
  static public bool IsNullableType(object theObject)
  {
    // Nullable value types implement the INullableValue interface.
    INullableValue iNullableValue = theObject as INullableValue;
    return iNullableValue != null;
  }
  // Return true if the object is either an object with a null value or a 
  // nullable reference type with a null value.
  static public bool IsNull(object theObject)
  {
    bool result = false;
    if (theObject == null)
    {
      result = true;
    }
    else
    {
      INullableValue iNullableValue = theObject as INullableValue;
      if (iNullableValue != null && !iNullableValue.HasValue)
      {
        result = true;
      }
    }
    return result;
  }
  // Return the nullable value type's value or "null".
  static public string ToString(object theObject)
  {
    Debug.Assert(IsNullableType(theObject));
    string result = "";
    // Nullable value types implement the INullableValue interface.
    INullableValue iNullableValue = theObject as INullableValue;
    if (iNullableValue != null)
    {
      if (iNullableValue.HasValue)
      {
        result = iNullableValue.ToString();
      }
      else
      {
        result = "null";
      }
    }
    return result;
  }
}
Back to article


Listing Three
 ...
partial class Form1 : Form
{
  public Form1()
  {
    InitializeComponent();
  }
  private void button1_Click(object sender, EventArgs e)
  {
    MessageBox.Show("Hello World");
  }
}
Back to article


Listing Four
 ...
partial class Form1
{
  ...
  private void InitializeComponent()
  {
    System.Windows.Forms.Label label1;
    this.button1 = new System.Windows.Forms.Button();
    label1 = new System.Windows.Forms.Label();
    this.SuspendLayout();
    // 
    // label1
    // 
    label1.AutoSize = true;
    label1.Location = new System.Drawing.Point(31, 34);
    label1.Name = "label1";
    ...
    // 
    // button1
    // 
    this.button1.Location = new System.Drawing.Point(112, 146);
    this.button1.Name = "button1";
    ...
    this.button1.Click += new System.EventHandler(this.button1_Click);
    ...
  }
  ...
  private System.Windows.Forms.Button button1;
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video