CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Jeffrey Palermo [MVP]

Software management consultant and CTO, Headspring Systems

Introducing the SmartBag for ASP.NET MVC. . . and soliciting feedback

To get objects from an ASP.NET MVC controller to a view, you put the objects in a dictionary of sorts called ViewData.  ViewData is sort of a misnomer because it's not meant to contain data at all.  It's meant to contain objects.   By the way, if you like this post, you can subscribe to my RSS at http://feeds.feedburner.com/jeffreypalermo.

There are some challenges with ViewData as it stands now:

  • A key is required for every object, both in the controller and view.
    ViewData.Add("conference", conference);
  • A cast is required to pull out object by key.
    (ScheduledConference)ViewData["conference"]
  • The ViewPage<T> solution discards the valuable flexibility of the objectbag being passed to the view.
    <%=ViewData.DaysUntilStart.ToString() %> where ViewData is of type ScheduledConference

Facts (or my strong opinions):

  • Repeated keys from controller and view increase chance for typos and runtime errors.
  • Casting every extraction of an object in the view is annoying.
  • Strong-typing ViewPage only works for trivial scenarios.
    - For instance, suppose once logged in, every view will need the currently logged in user.  Perhaps the user name is displayed at the top right of the screen in the layout (master page).  Since the layout shares the viewdata with the page, we immediately have the need for a flexible container that supports multiple objects.  A strongly typed ViewPage<T> won't work without an elaborate hierarchy of presentation object that are themselves flexible object containers able to support everything needed.  Once you get there, you are almost back to the initial dictionary.

My proposed draft solution, the SmartBag:  (and a follow-up here)

  • The SmartBag is currently only in CodeCampServer, but when it has proven it's worth, I'll move it into MvcContrib.  It's used in the ConferenceController and the ShowSchedule view.  You can check out CodeCampServer at http://codecampserver.org.

  • public class SmartBag : Hashtable
    {
    public T Get<T>()
    {
    return (T)Get(typeof(T));
    }

    public object Get(Type type)
    {
    if (!ContainsKey(type))
    {
    string message = string.Format("No object exists that is of type '{0}'.", type);
    throw new ArgumentException(message);
    }

    return this[type];
    }

    public void Add(object anObject)
    {
    Type type = anObject.GetType();
    if (ContainsKey(type))
    {
    string message = string.Format("You can only add one default object for type '{0}'.", type);
    throw new ArgumentException(message);
    }

    Add(type, anObject);
    }

    public bool Contains<T>()
    {
    return ContainsKey(typeof (T));
    }
  • The smartbag implements IDictionary through HashTable, so it's flexible, just like ViewData, but it has a powerful convention:  If you are passing one object/type to the view, we don't need a key because the SmartBag can use the type as the key.  If you really need a Top conference and a Bottom conference, then you are back to using your keys, but for 80% (statistic I pulled out of my behind) cases, this works.
  • If we introduce a layer supertype for our views, then we can just use it:

  • public class ViewBase : ViewPage<SmartBag>
    {

    }
  • Then, all my views inherit from ViewPage<SmartBag>.

I'd appreciate feedback on this solution.  To me, it seems to be a good foundation and convention, but I'm sure it can be improved.  If this solution hasn't been proven to be worthless, I'll port it to MvcContrib so it can be used by MvcContrib clients.


Published Jan 19 2008, 11:50 AM by Jeffrey Palermo
Filed under:

Comments

Javier Lozano said:

Why did you choose Hashtable rather than Dictionary<TKey, TValue> to implement SmartBag?

# January 19, 2008 1:46 PM

Oren Novotny said:

I'd come up with something similar when writing  LINQ provider -- the major difference is that it implements ICustomTypeDescriptor and which allows it to be databound easily w/o extracting the items:

public class ObjectSet : ICustomTypeDescriptor

   {

       Dictionary<Type, object> _typeMap;

       private ObjectSet()

       {

       }

       public ObjectSet(params object[] objects)

       {

           _typeMap = new Dictionary<Type, object>();

           _typeMap = objects.ToDictionary(o => o.GetType());

       }

       public T GetObject<T>()

       {

           return (T)_typeMap[typeof(T)];

       }

       public bool Contains<T>()

       {

           return _typeMap.ContainsKey(typeof(T));

       }

       /// <summary>

       /// Returns a new ObjectSet with the object added in

       /// </summary>

       /// <param name="obj"></param>

       /// <returns></returns>

       public ObjectSet AddOrUpdate(object obj)

       {

           if(obj == null)

               throw new ArgumentNullException("obj");

           var cs = new ObjectSet();

           cs._typeMap = new Dictionary<Type, object>(_typeMap);

           cs._typeMap[obj.GetType()] = obj;

           return cs;

       }

       /// <summary>

       /// Returns a new ObjectSet without the object

       /// </summary>

       /// <param name="obj"></param>

       /// <returns></returns>

       public ObjectSet Remove(object obj)

       {

           if (obj == null)

               throw new ArgumentNullException("obj");

           var cs = new ObjectSet();

           cs._typeMap = new Dictionary<Type, object>(_typeMap);

           cs._typeMap.Remove(obj.GetType());

           return cs;

       }

       #region ICustomTypeDescriptor Members

       AttributeCollection ICustomTypeDescriptor.GetAttributes()

       {

           return TypeDescriptor.GetAttributes(this, true);

       }

       string ICustomTypeDescriptor.GetClassName()

       {

           return TypeDescriptor.GetClassName(this, true);

       }

       string ICustomTypeDescriptor.GetComponentName()

       {

           return TypeDescriptor.GetComponentName(this, true);

       }

       TypeConverter ICustomTypeDescriptor.GetConverter()

       {

           return TypeDescriptor.GetConverter(this, true);

       }

       EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()

       {

           return TypeDescriptor.GetDefaultEvent(this, true);

       }

       PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()

       {

           return TypeDescriptor.GetDefaultProperty(this, true);

       }

       object ICustomTypeDescriptor.GetEditor(Type editorBaseType)

       {

           return TypeDescriptor.GetEditor(this, editorBaseType, true);

       }

       EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)

       {

           var eds = from kvp in _typeMap

                     from ed in TypeDescriptor.GetEvents(kvp.Value, attributes).Cast<EventDescriptor>()

                     select new KeyValuePair<EventDescriptor, Type>(ed, kvp.Value.GetType());

           return WrapEvents(eds);

       }

       EventDescriptorCollection ICustomTypeDescriptor.GetEvents()

       {

           var eds = from kvp in _typeMap

                     from ed in TypeDescriptor.GetEvents(kvp.Value).Cast<EventDescriptor>()

                     select new KeyValuePair<EventDescriptor, Type>(ed, kvp.Value.GetType());

           return WrapEvents(eds);

       }

       PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)

       {

           var pds = from kvp in _typeMap  

                     from pd in TypeDescriptor.GetProperties(kvp.Value, attributes).Cast<PropertyDescriptor>()

                     select new KeyValuePair<PropertyDescriptor, Type>(pd, kvp.Value.GetType());

           return WrapProperties(pds);            

       }

       PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()

       {

           var pds = from kvp in _typeMap

                     from pd in TypeDescriptor.GetProperties(kvp.Value).Cast<PropertyDescriptor>()

                     select new KeyValuePair<PropertyDescriptor, Type>(pd, kvp.Value.GetType());

           return WrapProperties(pds);

       }

       object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)

       {

           return this;

       }

       private static PropertyDescriptorCollection WrapProperties(IEnumerable<KeyValuePair<PropertyDescriptor, Type>> source)

       {

           var s = source.Select(kvp => new ObjectSetPropertyDescriptor(kvp.Key, kvp.Value)).ToArray();

           return new PropertyDescriptorCollection(s);

       }

       private static EventDescriptorCollection WrapEvents(IEnumerable<KeyValuePair<EventDescriptor, Type>> source)

       {

           var s = source.Select(kvp => new ObjectSetEventDescriptor(kvp.Key, kvp.Value)).ToArray();

           return new EventDescriptorCollection(s);

       }

       #endregion

       private sealed class ObjectSetEventDescriptor : EventDescriptor

       {

           private EventDescriptor _baseDesc;

           private Type _ownerType;

           public ObjectSetEventDescriptor(EventDescriptor ed, Type ownerType)

               : base(ed)

           {

               if(ed == null)

                   throw new ArgumentNullException("ed");

               if (ownerType == null)

                   throw new ArgumentNullException("ownerType");

               _ownerType = ownerType;

               _baseDesc = ed;

           }

           private object GetTarget(object source)

           {

               if (source == null)

                   throw new ArgumentNullException("source");

               ObjectSet os = source as ObjectSet;

               if (os != null)

                   return os._typeMap[_ownerType];

               if (source.GetType().Equals(_ownerType))

                   return source;

               throw new ArgumentException(string.Format("Type mismatch.  Source must be of Type {0}.", _ownerType), "source");

           }

           public override void AddEventHandler(object component, Delegate value)

           {

               _baseDesc.AddEventHandler(GetTarget(component), value);

           }

           public override Type ComponentType

           {

               get { return _baseDesc.ComponentType; }

           }

           public override Type EventType

           {

               get { return _baseDesc.EventType; }

           }

           public override bool IsMulticast

           {

               get { return _baseDesc.IsMulticast; }

           }

           public override void RemoveEventHandler(object component, Delegate value)

           {

               _baseDesc.RemoveEventHandler(GetTarget(component), value);

           }

           public override string DisplayName

           {

               get

               {

                   return string.Concat(_ownerType.Name, ".", base.DisplayName);

               }

           }

       }

       private sealed class ObjectSetPropertyDescriptor : PropertyDescriptor

       {

           private PropertyDescriptor _baseDesc;

           private Type _ownerType;

           public ObjectSetPropertyDescriptor(PropertyDescriptor pd, Type ownerType) : base(pd)

           {

               if (pd == null)

                   throw new ArgumentNullException("pd");

               if (ownerType == null)

                   throw new ArgumentNullException("ownerType");

               _ownerType = ownerType;

               _baseDesc = pd;

           }

           private object GetTarget(object source)

           {

               if(source == null)

                   throw new ArgumentNullException("source");

               ObjectSet os = source as ObjectSet;

               if (os != null)

                   return os._typeMap[_ownerType];

               if (source.GetType().Equals(_ownerType))

                   return source;

               throw new ArgumentException(string.Format("Type mismatch.  Source must be of Type {0}.", _ownerType), "source");

           }

           public override bool CanResetValue(object component)

           {

               return _baseDesc.CanResetValue(GetTarget(component));

           }

           public override Type ComponentType

           {

               get

               {

                   return _baseDesc.ComponentType;

               }

           }

           public override object GetValue(object component)

           {

               return _baseDesc.GetValue(GetTarget(component));

           }

           public override bool IsReadOnly

           {

               get { return _baseDesc.IsReadOnly; }

           }

           public override Type PropertyType

           {

               get { return _baseDesc.PropertyType; }

           }

           public override void ResetValue(object component)

           {

               _baseDesc.ResetValue(GetTarget(component));

           }

           public override void SetValue(object component, object value)

           {

               _baseDesc.SetValue(GetTarget(component), value);

           }

           public override bool ShouldSerializeValue(object component)

           {

               return _baseDesc.ShouldSerializeValue(GetTarget(component));

           }

           public override string DisplayName

           {

               get

               {

                   return string.Concat(_ownerType.Name, ".", base.DisplayName);

               }

           }

       }

   }

# January 19, 2008 2:04 PM

Brian Sullivan said:

Sounds good to me.  Anything that would keep me from using string-keyed collections and casting would be a plus.  That just gives me the willies.  Even if it turns out not to be the best solution, getting it out there in the ecosystem can only be beneficial.

# January 19, 2008 2:14 PM

Evan said:

You might think about adding an overload to allow passing in a key.. Your current design only allows passing 1 value per type in the bag..

T Get(); //use the type-specific default key

T Get(string key); // use a type-specific prefix + key

void Set<T>();  // use the type-specific default key

void Set<T>(string key); // use the type-specific prefix + key

# January 19, 2008 3:04 PM

Adam Tybor said:

It seems more simple than Castle's DictionaryAdapter and could cover, like you say, 80% of cases, but its still forcing you to create strongly typed viewdata objects.  

I prefer the DictionaryAdaper because I can just create my interfaces and use primitives like strings, and ints, and not have to worry about keys.  The advantage of interfaces over strongly typed objects is you can achieve multiple inheritance very easily.

You can certainly achieve the same thing with the SmartBag, it just adds extra line noise with the generic parameter and the get method.

ViewData.Get<SiteHeader>().Title as opposed to ViewData.SiteHeader with the DictionaryAdapter.

+1 to adding it to mvc contrib.

@Evan, I like that extension too.

# January 19, 2008 3:55 PM

Bill Pierce said:

"If you are passing one object/type to the view, we don't need a key because the SmartBag can use the type as the key"

How is this different than the current support for strongly typed ViewData?

# January 19, 2008 4:11 PM

Kyle Baley said:

Good idea and not just for passing ViewData. Seems like a good pattern whenever you need to pass any collection of heterogeneous objects around.

Have you seen PowerCollections: www.codeplex.com/PowerCollections

I haven't looked into it myself but reading through your code made me think of it. It may have something similar that you could pilfer.

# January 19, 2008 5:05 PM

Andy said:

Looks a great idea for MVCContrib. Perhaps it could be called ConventionViewData to accompany ConventionController? Though I suppose SmartBag sounds quite funky!

# January 19, 2008 5:16 PM

Jeffrey Palermo said:

@Bill,

This is different because with SmartBag, we can pass many objects to the view and use no key if we have only one of each type.  If we have two objects of the same type passed, then we'll have to use a key.

@Evan,

You are right.  I missed that method.  I'll be adding it.  

# January 20, 2008 12:30 AM

DotNetKicks.com said:

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# January 20, 2008 1:11 AM

Hernan said:

It sounds like a very good solution for me. The casting and the key's are a problem. I guest there is no way to avoid them altogether as Evan mention we still will need the key if we have to objects of the same type.

I will love to see this solution inside MvcContrib, it still bothers me that you may access to (let's say) Users objects one doing Get<User>() and another one doing Get<User>("key");. Not sure why, but I'm thinking on a third person reading the code three month later and trying to figure out why sometimes we pass a key and sometimes we don't.

# January 20, 2008 1:25 AM

eric hexter said:

I like the idea of this.  I would rather see it called TypedBag rather than smart bag.    The idea of making anything key based really feels like a smell to me.   I would rather see the creation of a DTO that includes all of the objects that are needed for a view / viewlet / component.  Than you can create a composite view by including multiple DTOs in the SmartBag.

I do like the Castle DictionaryAdaptor approach in that you could define your DTOs as interfaces and you never have to create concrete types.  The adaptor does that work for you.  You might want to look at that object to get a better feel for what it can do.

# January 20, 2008 9:22 AM

» Daily Bits - January 20, 2008 Alvin Ashcraft’s Daily Geek Bits: Daily links plus random ramblings about development, gadgets and raising rugrats. said:

Pingback from  &raquo; Daily Bits - January 20, 2008 Alvin Ashcraft&#8217;s Daily Geek Bits: Daily links plus random ramblings about development, gadgets and raising rugrats.

# January 20, 2008 10:09 AM

Kevin Isom said:

I must agree with what Eric said about the Monorail DictionaryAdaptor. Definately a nice way to do it.

# January 20, 2008 3:23 PM

Hernan said:

What bout this, instead of store the object in the hashtable directly, store an array of objects of the given type.

That way we won't need to use keys to get those objects.

Get<T>(index);

I also provide a First<t> method that returns the first one just to make client notation cleaner.

I also provide a CountOf<T> method (not crazy about the name but I don't want to override Count from the hashtable, this method returns the number of a given object.

What do you think?

Code below.

   public class SmartBag : Hashtable

   {

       public T First<T>()

       {

           return (T)this.Get(typeof(T), 0);

       }

       public int CountOf<T>()

       {

           Type type = typeof(T);

           if (!ContainsKey(type))

           {

               string message = string.Format("No object exists that is of type '{0}'.", type);

               throw new ArgumentException(message);

           }

           return ((ArrayList)this[type]).Count;

       }

       public T Get<T>(int index)

       {

           return (T)this.Get(typeof(T), index);

       }

       private object Get(Type type, int index)

       {

           if (!ContainsKey(type))

           {

               string message = string.Format("No object exists that is of type '{0}'.", type);

               throw new ArgumentException(message);

           }

           return ((ArrayList)this[type])[index];

       }

       public void Add(object anObject)

       {

           Type type = anObject.GetType();

           this.Add(type, anObject);

       }

       public override void  Add(object key, object value)

       {

           if (ContainsKey(key))

           {

               ((ArrayList)this[key]).Add(value);

           }

           else

           {

               ArrayList al = new ArrayList();

               al.Add(value);

               base.Add(key, al);

           }

       }

       public bool Contains<T>()

       {

           return ContainsKey(typeof(T));

       }

   }

# January 20, 2008 5:01 PM

Books and bits » Blog Archive » The SmartBag to replace the ViewData on MVC said:

Pingback from  Books and bits  &raquo; Blog Archive   &raquo; The SmartBag to replace the ViewData on MVC

# January 20, 2008 5:15 PM

Garry Shutler said:

I was going to make a similar suggestion to Hernan, basically making it support enumerations by each entry being a List of the type. Then the Get<T> would get the first element and there would be an EnumerationOf<T> method as well.

I personally don't see why you would need to retrieve elements by their index if you are using DTOs for the data you want to display.

# January 20, 2008 7:11 PM

Jeffrey Palermo said:

I've posted an update in CodeCampServer and converted all the conference views and actions to use it.  I think it comes out pretty clean.  Please comment on the new usage.

# January 20, 2008 10:08 PM

Christopher Steen said:

ASP.NET Forms Authentication and path= in the Tag [Via: Rick Strahl ] Sharepoint WSS List DataSource...

# January 21, 2008 12:10 AM

Christopher Steen said:

Link Listing - January 20, 2008

# January 21, 2008 12:10 AM

Matt Hinze said:

I've been trying to get a more robust option for ViewData implemented.  I like the SmartBag.  But using a Type based key isn't as beneficial as you might think.  It's very common to have multiple objects of the same type in the collection.  The first will populate the main page and a second for an associated viewlet.  Think a page for all products vs. a viewlet for My Products in the sidebar. They both use ProductsPresentationCollection or whatever. I had to extend the SmartBag to handle object based keys too.

[Test]

       public void ShouldRetrieveSingleObjectByKey()

       {

           SmartBag bag = new SmartBag();

           Guid guid = new Guid();

           Guid guid2 = new Guid();

           bag.Add(guid);

           bag.Add("key", guid2);

           Assert.That(bag.Get<Guid>("key"), Is.EqualTo(guid2));

       }

and

public class SmartBag : Hashtable

{

// ...

       public T Get<T>(object key)

       {

           if (!ContainsKey(key))

           {

               string message = string.Format("No object exists with key '{0}'.", key);

               throw new ArgumentException(message);

           }

           return (T) this[key];

       }

by the way, this is sort of similar to my PageData<T> class which inherits from ViewData class and has a ViewData generic property and an IDictionary(string,object) collection as a property called ViewletData.  It worked quite well because it nicely segregated the viewdata from the viewlet data.  See the Issue6 thread on the ccs-discuss list for more info.

Either way - there is some funny business occurring as the controller does more than control.  We now need a centralized contact-based mechanism for creating this massive SmartBag..  Certain site-wide presentation objects need to be added to the SmartBag even in seemingly unrelated controllers.  Which concerns me, as I do not want my repos querying for every request.

It's a fine solution for what we have now Jeremy.  But I think in the long run that RenderAction is a lot less messy and even, as our SmartBag gets bigger than we'd like to admit now that it will, more "performant"

The onus

I think enhanced viewdata like SmartBag or PageData

# January 21, 2008 8:50 AM

Ken Egozi said:

I'll second Adam.

Imo, every view should have an interface anyway. Having that, the use of the DictionaryAdapter is most natural.

So, you'd declare ViewBase<IView> { ... IView view = dictionaryAdapter.GetAdapter<IView>(ViewData); }

and have the dictionary adapter injected to your page with your IoC container of choice.

here:

www.kenegozi.com/.../typed-view-properties-in-monorail-and-aspview.aspx

is a sample for MonoRail/AspView.  This could be easily adapted to ASPNET.MVC (just swap propertybag with ViewData)

# January 21, 2008 9:20 AM

Greg said:

shouldn't this be encapsulating a hashtable/dictionary instead of BEING a hashtable/dictionary? Perhaps even meeting the interface? i.e. in the above how do I insure my invariant that there aren't "other things" in my dictionary since it is exposed?

also since this is to be used so often it could really benefit from being a dictionary where it could avoid dual lookups (contains then get etc) on every operation ....

further ... I need to be able to say StoreThisObjectAsThisType as opposed to it always using GetType() ... otherwise it defeats the purpose of polymorphism ... it would really suck that I want to add a proxy to AnObject and my view mysteriously start failing.

# January 21, 2008 12:10 PM

Wöchentliche Rundablage: .NET 3.5, WPF, LINQ, Tests, System.AddIn, SubSonic, Sandcastle | Code-Inside Blog said:

Pingback from  W&ouml;chentliche Rundablage: .NET 3.5, WPF, LINQ, Tests, System.AddIn, SubSonic, Sandcastle | Code-Inside Blog

# January 21, 2008 3:48 PM

Jeffrey Palermo said:

@Greg,

I think it IS an IDictionary, and Hashtable gives us that.  All Hashtable operations are still valid.

Regarding the adding as type:  You are completely correct.  This should support proxies, so we'll have to change it to store as explicit type and not derive the type of the actual object.  We need to be able to support polymorphism.

# January 22, 2008 12:02 PM

Greg said:

jeffrey: you miss my point ... if it IS a hashtable what prevents me from saying

Bag["Foo777"] = Something

or

myBag.Add("Bar", new object());

.

# January 22, 2008 12:52 PM

Jeffrey Palermo said:

@Greg,

Let me try again.  It is a hashtable, so

Bag["Foo777"] = Something is a perfectly valid scenario.

If you need the full flexibility of the IDictionary, then it is there to use.

# January 22, 2008 1:04 PM

Jeremy D. Miller said:

@Jeffrey,

If I can make a suggestion that *might* satisfy Greg.

Make it:

T Bag.Get<T>() or T Bag.Get<T>(key),

but not:

object Bag.Get(key)

To stay a little more true to the MVC theme, shouldn't you always be passing a single Model object to the View?  Passing a hashtable just seems like a great way to get yourself into trouble.  What's going on with the ConventionController idea?

# January 22, 2008 1:38 PM

Jeffrey Palermo said:

@Jeremy,

Done.

Single model:  No, realistic views are going to need a flexible number of objects, so we need a flexible container to pass.  One object is only sufficient if you have a presentation DTO for every view OR you have a trivial view.

ConventionController is alive and well in the MvcContrib project (http://mvccontrib.org)

# January 24, 2008 10:32 AM

Jeff Handley said:

This is excellent.  Second time sitting down to work on my first ASP.NET MVC project, I spent the entire evening trying to create a specific solution to this problem, not thinking about it being a generic problem.  The 'Current User' scenario is dead on... there's no reason for every Model passed down to have to know that the UI needs to have a User object.  We're going to end up having too much UI embedded into our Model that way.

I agree with sticking with the single model.  If you need to put multiple objects of the same type into the View, then just wrap those objects into a dictionary or array yourself and stick that collection into the bag.

Thanks!

# January 24, 2008 9:29 PM

Jeff Handley said:

ViewData seems too constraining

# January 24, 2008 10:06 PM

Jeffrey Palermo [MVP] said:

Earlier, I announced the advent of the SmartBag for the ASP.NET MVC Framework. Quite a few folks commented

# January 25, 2008 12:06 AM

Jeffrey Palermo said:

@Greg,

Check out my follow-up to see if I have it now.

codebetter.com/.../smartbag-for-asp-net-mvc-take-two.aspx

# January 25, 2008 8:08 AM

Community Blogs said:

A while back, I thought up the idea of the SmartBag , which has a very friendly API for working with

# March 26, 2008 5:04 PM

About Jeffrey Palermo

Jeffrey Palermo is a software management consultant and the CTO of Headspring Systems in Austin, TX. Jeffrey specializes in Agile coaching and helps companies double the productivity of software teams. Jeffrey is an MCSD.Net , Microsoft MVP, Certified Scrummaster, Austin .Net User Group leader, AgileAustin board member, INETA speaker, INETA Membership Mentor, Christian, husband, father, motorcyclist, Eagle Scout, U.S. Army Veteran, and Texas A&M University graduate. Check out Devlicio.us!

This Blog

Syndication