Welcome to MSDN Blogs Sign in | Join | Help

Enforcing Immutability in Code

A bunch of people (Eric, Wes, Joe, etc) have been writing about the wonders of immutable data structures lately.  A friend who no longer works at MS suggested that I should dig up some code that he wrote with some feedback from me to try and help you enforce immutability in your code.

We started with the idea that we could use attributes to mark types as immutable.  Then we could write some code that enforces that the Immutable attribute is actually satisfied.  The rules are:

  1. The base type meets the rules.
  2. The members are marked 'readonly'
  3. The types of the members meet the rules.

In practice, this fails very quickly, as soon as you bring in type that you didn't write, so there are some special exceptions:

  1. Builtin types get a pass
  2. 'enum' types get a pass
  3. There's a whitelist of types in .Net Framework that are immutable, but lack the attribute.
  4. You can put the [Immutable] attribute on a _field_, indicating that the usage of this field is correct wrt. immutability, even though the type of the field isn't itself immutable.
  5. You can put [Immutable (OnFaith=true)] on a type to say that a type should be assume to meet the rules without further inspection.
  6. In your test you can pass in an additional whitelist of types that you don't own, aren’t part of .Net Framework, and are immutable.

Here's the code for those who are interested.  I'll start with an example of problematic code we'd like to find.  There is a problem with both of the types below.  'MutableStruct' is a problem because any time you have a mutable value type you have a problem in my opinion.  It's far too easy to mutate the wrong copy of a struct, and spend ages trying to figure out where the bug is.  'MutableClass' is a problem because it lies.  The author placed the ImmutableAttribute on it, signifying that they intend the type to be immutable, but it's actually not.  The sort of time this is an issue is when you have a large amount of code which depends on the fact that MutableClass is immutable, and then you try to make a change that makes it mutable.  Without a system like this, you may not realize that the original author intended MutableClass to be immutable.

struct MutableStruct
{
    public int field;
}

[Immutable]
class MutableClass
{
    public int mutableField;
}

Next, let's write some unit tests, to ensure that we don't ship this code.  There are two unit tests here.  The first one ensures that all structs are marked with the immutable attribute.  The second verifies that all types marked with the ImmutableAttribute actually are immutable.  It does that by calling into some code directly in the attribute, which is below.  You might ask why this wasn't implemented as a Code Analysis, (aka FxCop) rule.  The simple answer is that Jay and I were familiar with reflection and the unit testing framework, but not familiar with writing FxCop rules.  If anyone wants to translate this into FxCop rules, I'd be interested in what that looks like.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class Immutable_Tests
{
    private IEnumerable<Assembly> AssembliesToTest
    {
        get { return new[] { System.Reflection.Assembly.GetExecutingAssembly() }; }
    }

    [TestMethod]
    // It's particularly important that 'struct' types are immutable.
    // for a short discussion, see http://blogs.msdn.com/jaybaz_ms/archive/2004/06/10/153023.aspx
    public void EnsureStructsAreImmutableTest()
    {
        var mutableStructs = from type in AssembliesToTest.GetTypes()
                             where IsMutableStruct(type)
                             select type;

        if (mutableStructs.Any())
        {
            Assert.Fail("'{0}' is a value type, but was not marked with the [Immutable] attribute", mutableStructs.First().FullName);
        }
    }
    [TestMethod]
    // ensure that any type marked [Immutable] has fields that are all immutable
    public void EnsureImmutableTypeFieldsAreMarkedImmutableTest()
    {
        try
        {
            ImmutableAttribute.VerifyTypesAreImmutable(AssembliesToTest);
        }
        catch (ImmutableAttribute.ImmutableFailureException ex)
        {
            Console.Write(FormatExceptionForAssert(ex));
            Assert.Fail("'{0}' failed the immutability test.  See output for details.", ex.Type.Name);
        }
    }
    internal static bool IsMutableStruct(Type type)
    {
        if (!type.IsValueType) { return false; }
        if (type.IsEnum) { return false; }
        if (type.IsSpecialName) { return false; }
        if (type.Name.StartsWith("__")) { return false; }
        if (ReflectionHelper.TypeHasAttribute<ImmutableAttribute>(type)) { return false; }
        return true;
    }

    static string FormatExceptionForAssert(Exception ex)
    {
        StringBuilder sb = new StringBuilder();

        string indent = "";

        for (; ex != null; ex = ex.InnerException)
        {
            sb.Append(indent);
            sb.AppendLine(ex.Message);

            indent = indent + "    ";
        }

        return sb.ToString();
    }
}

 

Without further ado, here's the ImmutableAttribute itself.  Note that some of the code is written to use the "ReflectionHelper", which is a utility class that provides some (IMO) useful wrappers around System.Reflection.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)]
[Serializable]
public sealed class ImmutableAttribute : Attribute
{

    // in some cases, a type is immutable but can't be proven as such.
    // in these cases, the developer can mark the type with [Immutable(true)]
    // and the code below will take it on faith that the type is immutable,
    // instead of testing explicitly.
    //
    // A common example is a type that contains a List<T>, but doesn't 
    // modify it after construction.
    //
    // TODO: replace this with a per-field attribute, to allow the 
    // immutability test to run over the rest of the type.
    public bool OnFaith;

    /// <summary>
    /// Ensures that all types in 'assemblies' that are marked 
    /// [Immutable] follow the rules for immutability.
    /// </summary>
    /// <exception cref="ImmutableFailureException">Thrown if a mutability issue appears.</exception>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
    public static void VerifyTypesAreImmutable(IEnumerable<Assembly> assemblies, params Type[] whiteList)
    {
        var typesMarkedImmutable = from type in assemblies.GetTypes()
                                   where IsMarkedImmutable(type)
                                   select type;

        foreach (var type in typesMarkedImmutable)
        {
            VerifyTypeIsImmutable(type, whiteList);
        }
    }

    static bool IsMarkedImmutable(Type type)
    {
        return ReflectionHelper.TypeHasAttribute<ImmutableAttribute>(type);
    }

    class WritableFieldException : ImmutableFailureException
    {
        protected WritableFieldException(SerializationInfo serializationInfo, StreamingContext streamingContext)
            : base(serializationInfo, streamingContext)
        {
        }

        internal WritableFieldException(FieldInfo fieldInfo)
            : base(fieldInfo.DeclaringType, FormatMessage(fieldInfo))
        {
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)")]
        static string FormatMessage(FieldInfo fieldInfo)
        {
            return string.Format("'{0}' is mutable because field '{1}' is not marked 'readonly'.", fieldInfo.DeclaringType, fieldInfo.Name);
        }
    }

    class MutableFieldException : ImmutableFailureException
    {
        protected MutableFieldException(SerializationInfo serializationInfo, StreamingContext streamingContext)
            : base(serializationInfo, streamingContext)
        {
        }

        internal MutableFieldException(FieldInfo fieldInfo, Exception inner)
            : base(fieldInfo.DeclaringType, FormatMessage(fieldInfo), inner)
        {
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object,System.Object)")]
        static string FormatMessage(FieldInfo fieldInfo)
        {
            return string.Format("'{0}' is mutable because '{1}' of type '{2}' is mutable.", fieldInfo.DeclaringType, fieldInfo.Name, fieldInfo.FieldType);
        }
    }

    class MutableBaseException : ImmutableFailureException
    {
        protected MutableBaseException(SerializationInfo serializationInfo, StreamingContext streamingContext)
            : base(serializationInfo, streamingContext)
        {
        }

        internal MutableBaseException(Type type, Exception inner)
            : base(type, FormatMessage(type), inner)
        {
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)")]
        static string FormatMessage(Type type)
        {
            return string.Format("'{0}' is mutable because its base type ('[{1}]') is mutable.", type, type.BaseType);
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")]
    public class ImmutableFailureException : Exception
    {
        public readonly Type Type;

        protected ImmutableFailureException(SerializationInfo serializationInfo, StreamingContext streamingContext)
            : base(serializationInfo, streamingContext)
        {
        }

        internal ImmutableFailureException(Type type, string message, Exception inner)
            : base(message, inner)
        {
            this.Type = type;
        }

        internal ImmutableFailureException(Type type, string message)
            : base(message)
        {
            this.Type = type;
        }
    }

    /// <summary>
    /// Ensures that 'type' follows the rules for immutability
    /// </summary>
    /// <exception cref="ImmutableFailureException">Thrown if a mutability issue appears.</exception>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
    public static void VerifyTypeIsImmutable(Type type, IEnumerable<Type> whiteList)
    {
        if (whiteList.Contains(type))
        {
            return;
        }

        if (IsWhiteListed(type))
        {
            return;
        }

        try
        {
            VerifyTypeIsImmutable(type.BaseType, whiteList);
        }
        catch (ImmutableFailureException ex)
        {
            throw new MutableBaseException(type, ex);
        }

        foreach (FieldInfo fieldInfo in ReflectionHelper.GetAllDeclaredInstanceFields(type))
        {
            if ((fieldInfo.Attributes & FieldAttributes.InitOnly) == 0)
            {
                throw new WritableFieldException(fieldInfo);
            }

            // if it's marked with [Immutable], that's good enough, as we
            // can be sure that these tests will all be applied to this type
            if (!IsMarkedImmutable(fieldInfo.FieldType))
            {
                try
                {
                    VerifyTypeIsImmutable(fieldInfo.FieldType, whiteList);
                }
                catch (ImmutableFailureException ex)
                {
                    throw new MutableFieldException(fieldInfo, ex);
                }
            }
        }
    }

    static bool IsWhiteListed(Type type)
    {
        if (type == typeof(Object))
        {
            return true;
        }

        if (type == typeof(String))
        {
            return true;
        }

        if (type == typeof(Guid))
        {
            return true;
        }

        if (type.IsEnum)
        {
            return true;
        }

        // bool, int, etc.
        if (type.IsPrimitive)
        {
            return true;
        }

        // override all checks on this type if [ImmutableAttribute(OnFaith=true)] is set
        ImmutableAttribute immutableAttribute = ReflectionHelper.GetCustomAttribute<ImmutableAttribute>(type);
        if (immutableAttribute != null && immutableAttribute.OnFaith)
        {
            return true;
        }

        return false;
    }
}

[Serializable]
internal static class ReflectionHelper
{
    /// <summary>
    /// Find all types in 'assembly' that derive from 'baseType'
    /// </summary>
    /// <owner>jayBaz</owner>
    internal static IEnumerable<Type> FindAllTypesThatDeriveFrom<TBase>(Assembly assembly)
    {
        return from type in assembly.GetTypes()
               where type.IsSubclassOf(typeof(TBase))
               select type;
    }

    /// <summary>
    /// Check if the given type has the given attribute on it.  Don't look at base classes.
    /// </summary>
    /// <owner>jayBaz</owner>
    internal static bool TypeHasAttribute<TAttribute>(Type type)
        where TAttribute : Attribute
    {
        return Attribute.IsDefined(type, typeof(TAttribute));
    }

    // I find that the default GetFields behavior is not suitable to my needs
    internal static IEnumerable<FieldInfo> GetAllDeclaredInstanceFields(Type type)
    {
        return type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
    }

    /// <summary>
    /// A typesafe wrapper for Attribute.GetCustomAttribute
    /// </summary>
    /// <remarks>TODO: add overloads for Assembly, Module, and ParameterInfo</remarks>
    internal static TAttribute GetCustomAttribute<TAttribute>(MemberInfo element)
        where TAttribute : Attribute
    {
        return (TAttribute)Attribute.GetCustomAttribute(element, typeof(TAttribute));
    }

    /// <summary>
    /// All types across multiple assemblies
    /// </summary>
    public static IEnumerable<Type> GetTypes(this IEnumerable<Assembly> assemblies)
    {
        return from assembly in assemblies
               from type in assembly.GetTypes()
               select type;
    }
}

 

What do you think?  Is this sort of verification of immutability useful?

Technorati Tags: ,,,

Performance issues Part Two: ???

Okay, anyone whose been holding their breath for the last 1.5 years for part two of this series, I'm going to have to disappoint you.  I have no idea anymore what it was that I was planning on writing about in Part 2 of that series.

 

However, I wanted to dust off the old blog, because I have an idea that I want to blog about.

Performance issues Part One: GetSuperType

So I said on Saturday that I would write a future post about the performance issues I've been seeing.  Instead I'm going to write two separate posts.  First because, I don't entirely understand both issues, and second, because I think this one might get a little bit long.  The first issue I noticed you won't find in either Beta1 or Beta2, because it was recently introduced.  The issue is that after you open a file, or make an edit outside of a method/property body, the next time that you try to bring up a completion list, it takes a really long time.  (about 5 seconds on my dev machine, which is pretty powerful).

The reason for this is a little complicated, so I'm going to go into some more background about the C# language service, much like Cyrus does in this post.  One of the things we do when bringing up a completion list is determine what methods and types to display in it.  We generally do that by walking up the scopes from the cursor position, and adding everything we see.  However, sometimes we do more than that.  For example, if you are in a bases and interfaces list, and not on the first type in the list, we'll only show namespaces and interfaces or types that contain nested interfaces.  In order to do that, we ask each type: are you an interface, or do you have a nested interface, or a nested type that contains a nested interface.  We need to show types that have nested types that have interfaces, because you may want to type that nested interface, and if we don't show you the containing type, the completion list is just getting in your way.  Pretty simple really. 

The problem is that our logic wasn't quite right.  The issue is that it's possible to access a nested type in one of your super types, and we didn't put those nested types in the list.  Type for an example I think:

class Base {

    public interface IBase {

    }

}

 

class Derived : Base {

}

 

class C {}

 

class D : C, // HERE {

}

At the comment, it's valid to type "Derived.IBase", but we wouldn't show "Derived" in the completion list.  In order to fix this issue, we added code to an internal function "GetSuperType()", and checked to see if it has any nested types we would want to show.

Okay, so why does that create a perf problem you might ask.  The problem has to do with our implementation of GetSuperType.  Getting the super type of an object is a fairly expensive operation for us, because we essentially need to reparse the file to find it's bases and interfaces list, and then determine the new super type.  Once we find the super type, we then cache it for future lookups, which helps out performance most of the time.  The problem is that every time we reparse an entire file, we clear the cache of every super type.  That's because adding or removing a type may cause what the super type is to change (a new type closer in scope with the same name might be added, meaning we should re-bind to that type for example).

In my example above, when you open a file, we re-parse the file, in order to do things like populate the error-list, calculate the collapsible regions, and populate the navigation bar (those two drop-downs at the top of the file).  This causes our cache to be invalidated, and the next time you need a completion list, we have to go and re-bind the super type for every type in your containment hierarchy, and any nested types inside them, which causes quite a hit.

Okay, so how are we going to address the situation?  Well, we have three ideas.

  1. We can keep the parse tree's for all the files we need to parse alive while we calculate all of the super types, instead of re-parsing for each type we find.  Hopefully this will be enough.
  2. If it's not enough, we think we have a way to clear the cache must less aggressively.  Basically, when a type is removed from our knowledge (which happens when we parse, we remove it, then re-add it), we currently flush the cache of every supertype in the system.  In actuality, we only need to flush the cache of the types of which it is a supertype.  The problem is that currently a type doesn't know what types derive from it in our architecture.  We may need to add this information, but changing a core area like our type system is not something we would like to do.
  3. We could regress to the behaviour we have in Beta1 and Beta2, and just not show derived classes whose supertypes have nested classes we want to show.

We're currently investigating option number 1. What do you all think.

 

Finally! VS2005 Beta2 released.

We finally got it out there.

Take a look at msdn.

Or, to see it from the big guy, Soma's blog.

Please go get this, and let us know what you think.

Winforms, TechEd, Agile2005.

It's been ages since I've posted anything here, but I'm going to try and get back into the swing of things.

So what's new with me.

  1. For the last week or so, I've been helping out the Winforms team, instead of working on the C# IDE.  This has been pretty interesting for me, for a couple of reasons.  First, it means I get to actually use C#, instead of C++.  This has been fantastic for me, because it means that I get to see what the product I've been working on for the last 2.5 years is actually like.  I have to say that I like most of the stuff we've done, but I have found a few issues where performance hasn't been satisfactory, although Cyrus another person on our team, and I have some ideas for how they can be better.  Maybe I'll do another post explaining what the problems were, and what our ideas are for how to fix them.  Mostly I've been working on some Accessibility bugs, and also some issues with the ToolStrip controls.
  2. I'm going to TechEd 2005 in Orlando.  I'm really pretty excited about this, as it'll be my first chance to attend a big computer show.  I'm not speaking or anything, just working at the booth and the Cabana.
  3. I'll also be attending Agile2005 this year, which should also be a lot of fun, as it's much smaller, and I'm guessing many of the same people I met last year will be there again, and I'll get another chance to talk to them.

Well, I don't have much time to write right now, as my wife is in the process of picking my in-laws up from the airport and I'm supposed to be tidying up.

Monday @ Agile/XP Universe.

Not much to report for today, as I didn't have much of a chace to attend the show content.  I was kept pretty busy at the Microsoft booth talking to people and showing them some of the new stuff we've been building for Whidbey.

Tomorrow should be an exciting day, as Herb Sutter is presenting on C++/CLI and it's relevance to modern programming, and the agile community.  At the same time Alex Goyen is giving a talk about the Microsoft Solutions framework (v4), and using Visual Studio as a platform for agile development.

At Agile/XP Universe.

So today I arrived in Calgary for the Agile/XP Universe conference.  I'm really excited to be here and get a chance to talk to some of the people who are here.

Had quite a day of travelling, as I got a shuttle to the airport this morning.  It picked me up at 5:50 am for my 9:48 flight.  I cleared security before 6:45, and had to wait 3 hours before my flight left.  Then I landed in Edmonton for an hour, and went looking through the airport for some Tim Hortons.  Of course all I could find was a Seattle's Best Coffee (ah, the irony).  Anyway, I got to the hotel at about 4:00pm, called an Aunt and Uncle who live here in Calgary (and who I haven't seen for about 10 years) and made plans to go out for dinner at 6:30.  Then I decided to go down and check out the conference area.  So down I go to the 3rd floor of the Hyatt.  I stepped out of the elevator and there were a bunch of people standing around, so I went to register.  Apparently, I was the first person from Microsoft to arrive, so they asked me to set up our booth.  Now I've never been to a conference except as a regular guest, so this was a new one to me, but finally I got it all set up, and went to the intro session, where I volunteered to talk to some people about Refactoring support in Visual Studio tomorrow afternoon.  At that point it was 6:20, so I rushed down to the lobby and met my Aunt and Uncle for a nice dinner a little pub just down the street from the hotel.  Then back to the room to talk to my wife for a few minutes, and finally a quick swim.  Now I'm heading off to bed.

Just wanted to let you all know that I'm here, and I'll try to fill you in about what's going here at the conference.

When the conference is over, I'll be heading off to Toronto (my home town) for 2 weeks vacation, including my wife's and my 5th anniversary next Saturday.

Anonymous method formatting, again.

A while back I asked for some feedback about how we should format anonymous methods.  Then I was asking whether the default should be to put the opening brace on the same line as the delegate keyword or not.  From your feedback, it looks like we'll keep the default the same as it is now (on the next line).  So that brings me to another question.  Some people have requested the ability to have the anonymous method braces indented.  I'll give some examples:

Currently, anonymous methods look something like this:

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

      { return thing.ShouldBeIncluded; }

      );

 

Or this:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

      { return thing.ShouldBeIncluded; });

 

Or this:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

      {

            return thing.ShouldBeIncluded;

      });

 

Depending on where you put newlines.

 

With the proposed option, you would be able to make those look like:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

            { return thing.ShouldBeIncluded; }

      );

 

Or this:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

            { return thing.ShouldBeIncluded; });

 

Or this:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

            {

                  return thing.ShouldBeIncluded;

            });

 

Or even this:

 

      List<Thing> somethings = things.FindAll(delegate(Thing thing)

            {

                  return thing.ShouldBeIncluded;

            }

      );

 

 

So my question for you today is: Do you want the new option? If you do, what should the default be, indented, or not indented?

 

Please post and let me know.

 

Looking for feedback on some formatting changes

We’re considering making two changes to the way the formatting engine works, based on feedback we’ve received from Beta 1.  I’d like to post them here, and see what you all think of them, to make sure we’re making the right decision.

 

Change 1: Comments that start at column 1.

In Beta 1, if you have a comment that starts at column 1 (the very left edge of the screen), we won’t change it’s indentation.  The main reason for this behaviour is that if you select a block of text and use Edit->Advanced->Comment selection, we place all the ‘//’ that we generate at the left edge, and we wouldn’t want a subsequent formatting to push them way in.

 

We’re finding that this behaviour has bad effects in two main ways.

  1. It’s confusing.  It’s not really clear why we change the indentation of some comments, but not others.
  2. It has a negative effect on generated code.  Often it’s nice to be able to generate some code with comments in it(say with a snippet).  But it looks kind of ugly if either all you’re comments end up at the left margin, or you need to add spaces to the snippet file.

 

Our proposed solution to this is to make the formatting engine always indent comments, and fix our “Comment Selection” behaviour to generate comments at the indent of the line with the least indentation in a block.  For example comment selection would now generate something like this:

    public void Method()

    {

        List<int> list = new List<int>();

        li.Add(3);

        //foreach (int i in list)

        //{

        //    Console.WriteLine(i);

        //}

    }

Instead of:

    public void Method()

    {

        List<int> list = new List<int>();

        li.Add(3);

//      foreach (int i in list)

//      {

//          Console.WriteLine(i);

//      }

    }

 

Change 2: Anonymous methods default to having the open brace on the same line.

This change is mainly for readability.  Basically we want to change the default value of “Tools->Options->Text Editor->C#->Formatting->New Lines->New line options for braces->Place open brace on new line for anonymous methods” from true to false.  The reason for this is that we think that if you have code like this:

 

    List<Thing> things;

    void SomeMethod()

    {

        List<Thing> somethings = things.FindAll(delegate(Thing thing) {

            return thing.ShouldBeIncluded;

        });

    }

It’s easier to tell that it’s part of an anonymous method than:

        List<Thing> somethings = things.FindAll(delegate(Thing thing)

        { return thing.ShouldBeIncluded; }

        );

Or:

        List<Thing> somethings = things.FindAll(delegate(Thing thing)

        { return thing.ShouldBeIncluded; });

Or:

        List<Thing> somethings = things.FindAll(delegate(Thing thing)

        {

            return thing.ShouldBeIncluded;

        });

 

All of which look sort of like you just have a random block.

 

So what do you think?  Are we on the right track, or is there a better way to solve these problems?

 

C# Editor features you can blame on me.

Now that the Express products are available, and Beta 1 is out the door, I thought I would let you know what features you can blame me for.

Basically they are:

  • Auto Formatting in C#.
  • Auto complete on identifier.
  • Cyrus and I both worked a lot on putting keywords in completion lists, and filtering them to the appropriate ones.
  • ASP.net 2.0 integration with C#.\
  • C# Tools->Options pages.

Let's talk about each of them briefly:

Auto Formatting

With this feature, we will automatically format your source code, whenever you:

  • Paste.
  • Type a }
  • Type a ;
  • Issue a “Format Selection“, or “Format Document“ command.
  • Use “Uncomment Selection“

This formatting now goes beyond setting the indentation.  It also actually changes the whitespace between tokens.  In order to control this, there are quite a few options at Tools->Options->Text Editor->C#->Formatting.  If you find that the way you like to program isn't supported by the formatting engine, please let us know at the Feedback Center.  If you let us know about it fairly soon, we may be able to add it before we ship, otherwise, you're stuck with what you've got.  Finally, here's something to watch out for:  We only format if we can understand your code.  That means that if there is a red squiggle inside a function, we won't format that function, and if there is a red squiggle outside of a function, we won't format that file.  It really helps intellisense out if you can keep your code in a parsable state, so I'd recommend you just always try to keep it that way.

Autocomplete on identifier

This is the feature that if you start typing a new name, we bring up intellisense immediately, instead of waiting for you to type a dot.  I personally really like this, although there is a reason why some people don't like it.  It can make it hard to type the name of something that you know doesn't exist yet.  This can make it harder to use Generate Method Stub, since you may complete to something else.  If you find that you run into that type of situation a lot, there are two things you can do.  1. Tell us about it. 2. Turn the option off at Tools->Options->Text Editor->C#->Intellisense.

Keywords in completion lists

Not much to say here, except that we basically had to do this to support Autocomplete on identifier.  We try to filter the available keywords to what's legal to type at that point, so please let us know if you're favorite keyword isn't in the list, and should be.

ASP.net 2.0 integration with C#

I've already talked about this enough for now.  Please let us know if there are any issues.

C# Tools->Options pages

Again, not much to say about these, other than that they are now written in C#, instead of C++.  Once again, let me know if you find any issues.

Lastly, please report any issues that you find using the product now, while there is still time to change it.

Formatting all C# files in a solution

Formatting All Files in a Solution

 

One of the features I’ve worked on quite a bit during Whidbey is automatic formatting of source code.  We’ve done quite a bit of work in this area including:

  • Making formatting affect more than just indentation
  • Adding many more options about how code is formatted.
  • Making sure that code which is generated by VS is formatted according to your settings.

 

Because of this, we also added the Format Document command, which will format the entire current document, instead of just the selection.

 

One of the things we thought about adding a command to format the entire project, or the entire solution.  We decided not to do it, because we figured that if you were consistently using VS, there wouldn’t be much need, since the formatting should be right already.

 

So then, a little while ago, the C# profile changed its default so that spaces are used for indentation instead of tabs.  I personally prefer this, but most of my side projects had been written using tabs, because that was the old default, and when I was working on a lot of those projects, the Import/Export Settings feature wasn’t working, so I just left the defaults.  Please, I don’t need a bunch of comments about the default options in the profile.  Joe or Anson’s blogs are much better places for that.

 

So now I have a solution where all the new code has spaces, but the old code has tabs.  I wanted to convert all of the files to use spaces, but didn’t want to manually open and issue a Format Document command for about 100 files.

 

Instead, I broke out the VS Macro IDE, and decided to write a macro to do it.  After struggling with VB’s variable declaration syntax for about an hour, I finally came up with this:

 

    Public Sub FormatAll()

        Dim sol As Solution = DTE.Solution

        For i As Integer = 1 To sol.Projects.Count

            Dim proj As Project = sol.Projects.Item(i)

            For j As Integer = 1 To proj.ProjectItems.Count

                FormatSome(proj.ProjectItems.Item(j))

            Next

        Next

    End Sub

 

    Private Sub FormatSome(ByVal projectItem As ProjectItem)

        If projectItem.Kind = Constants.vsProjectItemKindPhysicalFile Then

            If projectItem.Name.LastIndexOf(".cs") = projectItem.Name.Length - 3 Then

                Dim window As Window = projectItem.Open(Constants.vsViewKindCode)

                window.Activate()

                projectItem.Document.DTE.ExecuteCommand("Edit.FormatDocument")

                window.Close(vsSaveChanges.vsSaveChangesYes)

            End If

        End If

 

        For i As Integer = 1 To projectItem.ProjectItems.Count

            FormatSome(projectItem.ProjectItems.Item(i))

        Next

    End Sub

 

 

To use it, launch VS, then go to Tools->Macros->Macros IDE.  Paste the code inside the Module, and then close the Macros IDE.  Next go to Tools->Macros->Macros Explorer, and double click the “FormatAll” macro.

 

Presto: All files in the solution formatted according to your settings.

 

NOTE: This macro has been tested on exactly 1 solution.  It will close all files during processing, and it may or may not work if your projects are under source control.  Use at your own risk.

C# and ASP.net Whidbey

It’s been a while, but now that ZBB is over, and I’ve got a little bit of time, I thought I would write a little about what has been occupying so much of my time lately.

 

C# and ASP.net Whidbey

 

One of the things I’ve been working on a lot lately is the interaction between C# and ASP.net for Whidbey, so I thought I would write a little bit about what’s going on in that space.

 

What’s new?

 

One of the major differences (at least as far as it’s impact on me goes :) is that in Whidbey, you will be able to create inline blocks of server executed script, just like in the old days of ASP.  Well not quite, because in ASP.net Whidbey, you will also get support from the individual language of your script block.  This means that colorization, intellisense, and all of those other language specific features should also work in inline script.

 

What does it look like?

 

Let’s say you create a web form with a simple button on it.  The code for the page might look something like this:

 

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

 

<script runat="server">

    uint _numClicks = 0;

   

    void Button1_Click(object sender, EventArgs e)

    {

        ++this._numClicks;

        this.Button1.Text = this._numClicks.ToString();

    }

</script>

 

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

    <title>Untitled Page</title>

</head>

<body>

    <form id="form1" runat="server">

    <div>

        <asp:Button ID="Button1" Runat="server" Text="Button" OnClick="Button1_Click" />

    </div>

    </form>

</body>

</html>

 

 

How does it work?

 

What happens behind the scenes is that ASP.net generates a complete class which you as the user never see.  The C# language service however, sees both of these two things: The actual default.aspx file (what we call the primary buffer), and the C# class containing the inline as well as some other stuff to pull it all together.

 

This makes it a little difficult for the language service team, because whenever we look at a position in the file, we need to think about which file we’re looking at: it could be either the primary or the secondary buffer depending on what interface we’re talking through.  Generally, if we’re talking to either ASP.net team, or the core editor, we need to use primary buffer co-ordinates, but if we’re talking to the C# compiler we need to use secondary buffer co-ordinates.

 

We’ve pretty much adopted the rule that whenever we talk to the core team or the asp.net team, we’ll translate to/from primary buffer co-ordinates, but the rest of the time we’ll use secondary buffer co-ordinates throughout the language service and compiler.  This has been the source of many bugs in our ASP.net integration, because throughout the 7.0 and 7.1 (or RTM and Everett as we call them), cycles, we had an implicit assumption that the two sets of co-ordinate systems would be identical.  Thus most of the bugs in C# and ASP.net integration have come from places where we missed the fact that we were interfacing with another component.

 

Project system issues

 

Another interesting area when it comes to C# and ASP.net integration is how we interact with the project system.  Within the ASP.net world, each individual web form looks to the language service like a separate project.  The reason for this is that it’s not possible to reference code from a different page, and the only way to enforce this in the C# compiler is to have them generate different assemblies.  The only way to get them to generate different assemblies is to use different projects.  This isn’t really a big deal; it just means that opening a web form in the editor is a little bit more expensive than opening a normal C# file in the editor. 

 

What is interesting is that the project system only tells the language service and the compiler about a page when the page is open.  There are good reasons for this.  Imagine you are working against a remote web server, and the site you are working on contains hundreds of pages.  You don’t want VS to have to hit the webserver to retrieve every page when you open the solution.  Instead, VS hits the server only on demand, when you actually try to open the page.

 

“So what?” You might ask.  “How will this affect me?”

 

Well, this affects quite a number of C# Intellisense features, specifically, those that deal with finding things in code.  The types of things I’m talking about are the new “Find all references” command, and the new Refactorings.  The basic problem is that if a page is closed, we have no idea that it exists, so we can’t look for references to things in it.

 

Let’s take an example.  Imagine you have a class in your Code Directory (the shared repository all pages can access), and you have a reference to it in a bunch of web pages.  You decide that you don’t really like the name of the class so you decide to rename it.  You can use C#’s handy new Refactor->Rename command to do it.  The problem is that if all of the pages that reference it are not open, we don’t know they exist, so we don’t look for references in them, and they don’t get updated.  This isn’t a problem for refactorings of things defined in a web form, since they can’t be shared with other pages, but it is a problem for things defined in the Code directory.

Yet another new blogger!

Hello All,

My name is Kevin Pilch-Bisson.  I'm a Canadian living in the US, working as a developer for Microsoft on the C# ide team.  I've been here for almost 1.5 years.  I have 3 small children, and enjoy spending my spare time with them.  I often have a hard time describing exactly what our team is responsible for.  Normally I respond by saying that we are NOT responsible for the core editor of VisualStudio, but we are responsible for all of the language specific customizations that you see for C#.  The list usually goes something like this.  We ARE responsible for:

  • Items in completion lists (but not the drawing of the list itself).
  • What happens when you select something in a completion list
  • The contents of Parameter Help and Quick Info
  • Colorizing
  • Formatting
  • Task list contents
  • Class view contents
  • Navigation (go to definition, go to reference, etc)
  • The glue connecting the compiler to the project system
  • Refactoring (new for Whidbey)
  • Expansions (new for Whidbey)

We are NOT responsible for:

  • Search and replace
  • General look and feel of the product
  • The project system

I'm not really sure what I'm going to write about in this space, so we'll just have to see how things go.  Within the list above, I work on Formatting, our settings pages under Tools->Options, task list, and some stuff to do with colorizing, completion lists and parameter help.

More Posts « Previous page
 
Page view tracker