<p>I was able to implement a thread-safe Dictionary in C# by deriving from IDictionary and defining a private SyncRoot object:</p>

<pre><code>public class SafeDictionary&lt;TKey, TValue&gt;: IDictionary&lt;TKey, TValue&gt;
{
    private readonly object syncRoot = new object();
    private Dictionary&lt;TKey, TValue&gt; d = new Dictionary&lt;TKey, TValue&gt;();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}
</code></pre>

<p>I then lock on this SyncRoot object throughout my consumers (multiple threads):</p>

<p>Example:</p>

<pre><code>lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}
</code></pre>

<p>I was able to make it work, but this resulted in some ugly code.  My question is, is there a better, more elegant way of implementing a thread-safe Dictionary?</p>

You don't need to lock the SyncRoot property in your consumer objects.  The lock you have within the methods of the dictionary is sufficient.
To Elaborate:
What ends up happening is that your dictionary is locked for a longer period of time than is necessary.
What happens in your case is the following:
Say thread A acquires the lock on SyncRoot
before
the call to m_mySharedDictionary.Add.  Thread B then attempts to acquire the lock but is blocked.  In fact, all other threads are blocked.  Thread A is allowed to call into the Add method.  At the lock statement within the Add method, thread A is allowed to obtain the lock again because it already owns it.  Upon exiting the lock context within the method and then outside the method, thread A has released all locks allowing other threads to continue.
You can simply allow any consumer to call into the Add method as the lock statement within your SharedDictionary class Add method will have the same effect.  At this point in time, you have redundant locking.  You would only lock on SyncRoot outside of one of the dictionary methods if you had to perform two operations on the dictionary object that needed to be guaranteed to occur consecutively.