ASP.NET MVC 3 DropDownListFor strange behaviour for determining selected option

So a strange behaviour in the implementation of the HtmlHelper.DropDownListFor extension method was recently brought to my attention where the order of ‘looking for’ the selected value of the list seemed incorrect.

I would assume that the selected value would always be the selected value from the strongly typed model property (as I have clearly stated by providing a lambda which results in returning that property) but it seems the selected value will not always be the selected value from the strongly typed model as I would expect.

For example:

@model TestModel

@{
    Model.Title = "Hello";
    ViewBag.Title = "Tim";
    Layout = null;
}

@using (Html.BeginForm())
{
    @Html.DropDownListFor(p => p.Title, new SelectList(new[] { "Other", "Hello", "Brent","Tim","Smith" }, "Smith"))
}

Given the above code what would the value of the drop down list be?
a) “Smith”
b) “Tim”
c) “Hello”

Well I would expect the SelectList constructor to create a list with a selected value of “Smith” then I would expect the DropDownListFor extension method to set the selected value to be “Hello” as provided by my model.

In fact what happens is that the value from the ViewBag is taken in priority over any others!
The answer to above is actually b).

To be honest using a strongly typed view I don’t think I would ever want the extension method to take the value from the ViewBag implicitly.

Now if I remove the Title parameter in the ViewBag as done so below, what would you expect?

@model TestModel

@{
    Model.Title = "Hello";
    //ViewBag.Title = "Tim";
    Layout = null;
}

@using (Html.BeginForm())
{
    @Html.DropDownListFor(p => p.Title, new SelectList(new[] { "Other", "Hello", "Brent","Tim","Smith" }, "Smith"))
}

Using the above logic the value would be “Hello” which is correct. However this doesn’t give us an option to override the selected value as the SelectList’s selected overload does the work before handing the object off to the DropDownListFor extension method which it then ignores.

Finally if the ViewBag property is null AND my Model’s property is null then the SelectList’s selected value will be unchanged and the result will be “Smith”.

Now I looked into TextBoxFor and EditorFor, etc and they work as expected. It is only the ListBoxFor and DropDownListFor extension methods which are first checking for the value in the ViewBag before the Model.

Just something everyone needs to be aware of. I would consider this a bug. Be sure to check your naming as we had a page title in the viewbag called “ViewBag.Title” and a “Person.Title” property on our strongly typed Person model. The select list tried to set the selected value of the list to be the page’s title!.

ASP.NET MVC JQuery EventAggregator (Decoupled Event Handling)

December 6, 2011 Leave a comment

The concept of Event Aggregation was first introduced to me as I read through the PRISM toolkit (Composite Application Guidance) from the Microsoft Patterns and Practises team.

It allows you to modularise your code and reduce dependencies between modules, I love it.
An example of how it benefits web applications would be:

Consider a simple 2 column web page layout where I have a div with content on the left (LeftBarRegion) and a div for the more content (MainContentRegion) on the right. Lets say that these two regions are defined in a Layout page (or Master Page) and the inner content of the MainContentRegion is changed by a JQuery ajax call. I have a LeftView and RightView which would fill the appropriate regions in my layout.

Now how do I get the view in the LeftBarRegion to talk to the view in the MainContentRegion and vice versa without knowing anything about the implementation details of each view? What if I also want to notify the parent layout page from either of the inner containers?

Welcome to the power of EventAggregator. We could have any content we like in either of the regions above and they can still talk to each other in a de-coupled fashion.

Enough talking lets watch it in action.


// Very simple - JQuery awesomeness
var EventAggregator = {

    // Ensure you are using the jquery data from the parent document so that everything goes
    // through the same EventAggregator.
    publish: function (event, args) {
        window.top.document.$(window.top.document).trigger(event, args);
    },

    subscribe: function (event, delegate) {
        window.top.document.$(window.top.document).bind(event, delegate)
    }
}

Now I can do this:


// In left content page
EventAggregator.publish(Events.SomeEventThatNeedsToBeHandled, { ID = 1 });

// In content page
EventAggregator.subscribe(Events.SomeEventThatNeedsToBeHandled, function (args) {
    // Do stuff in the main content page when this event occurs
});

// In layout page
EventAggregator.subscribe(Events.SomeEventThatNeedsToBeHandled, function (args) {
    // Do stuff in the layout page when this event occurs.
});

Generic Repository: Fake IDbSet implementation update (Find Method & Identity key)

November 30, 2011 6 comments

UPDATE: Thanks to Eli Weinstock-Herman for pointing out the fact that Find should return null if no result is found (SingleOrDefault instead of Single). Cheers Eli.

Hey guys,

I’ve been back in the coding seat lately creating a new generic repository for a system that we are building. I’ve made some improvements to the FakeDbSet that I posted about earlier Here.

I want to add some notes to the previous post which are long enough they warrant a new post. Firstly, IT IS MUCH EASIER if you do not use foreign keys in your objects but instead use ‘association’ object references. This means you will not have to co-ordinate two different fields when setting up test data. Of course EF does this for you when connected to the database but in memory you would have to do this yourself.

Secondly the implementation of find was quite hard, though I believe I have come up with an elegant generic solution. If you look at the IDbSet documentation MSDN you will see that Find() expects the keys to passed in “the same order that they are defined in the model”.

If I use reflection to find my key properties I can then iterate through the keys and ensure that each object given in the find method equals the value of that key, as shown below.


        private List<PropertyInfo> _keyProperties;

        public virtual T Find(params object[] keyValues)
        {
            if (keyValues.Length != _keyProperties.Count)
                throw new ArgumentException("Incorrect number of keys passed to find method");

            IQueryable<T> keyQuery = this.AsQueryable<T>();
            for (int i = 0; i < keyValues.Length; i++)
            {
                var x = i; // nested linq
                keyQuery = keyQuery
                   .Where(entity => _keyProperties[x].GetValue(entity, null).Equals(keyValues[x]));
            }

            return keyQuery.SingleOrDefault();
        }

        private void GetKeyProperties()
        {
            _keyProperties = new List<PropertyInfo>();
            PropertyInfo[] properties = typeof(T).GetProperties();
            foreach (PropertyInfo property in properties)
            {
                foreach (Attribute attribute in property.GetCustomAttributes(true))
                {
                    if (attribute is KeyAttribute)
                    {
                        _keyProperties.Add(property);
                    }
                }
            }
        }

Now thirdly I wanted the FakeDbSet to act like the database and use an identity column for properties that are ints and marked with the [Key] attribute. I made these changes here

private int _identity = 1;

private void GenerateId(T entity)
{
     // If non-composite integer key
     if (_keyProperties.Count == 1 && _keyProperties[0].PropertyType == typeof(Int32))
         _keyProperties[0].SetValue(entity, _identity++, null);
}

  public T Add(T item)
  {
      GenerateId(item);
      _data.Add(item);
      return item;
 }

Now of course this is being done in the Add method not the commit method as the database would. For my purposes this makes no difference. If however you want the key generation to be done on commit then you need to keep an un-comitted list inside of the FakeDbSet and then when commit is called you would iterate the list generating id’s for each element and then adding them to the ‘comitted’ list.

Here is the new FakeDbSet implementation


public class FakeDbSet<T> : IDbSet<T> where T : class
    {
        private readonly HashSet<T> _data;
        private readonly IQueryable _query;
		private int _identity = 1;
        private List<PropertyInfo> _keyProperties;

        private void GetKeyProperties()
        {
            _keyProperties = new List<PropertyInfo>();
            PropertyInfo[] properties = typeof(T).GetProperties();
            foreach (PropertyInfo property in properties)
            {
                foreach (Attribute attribute in property.GetCustomAttributes(true))
                {
                    if (attribute is KeyAttribute)
                    {
                        _keyProperties.Add(property);
                    }
                }
            }
        }

		private void GenerateId(T entity)
		{
            // If non-composite integer key
            if (_keyProperties.Count == 1 && _keyProperties[0].PropertyType == typeof(Int32))
                _keyProperties[0].SetValue(entity, _identity++, null);
		}

        public FakeDbSet(IEnumerable<T> startData = null)
        {
            GetKeyProperties();
			_data = (startData != null ? new HashSet<T>(startData) : new HashSet<T>());
            _query = _data.AsQueryable();
        }

        public virtual T Find(params object[] keyValues)
        {
            if (keyValues.Length != _keyProperties.Count)
                throw new ArgumentException("Incorrect number of keys passed to find method");

            IQueryable<T> keyQuery = this.AsQueryable<T>();
            for (int i = 0; i < keyValues.Length; i++)
            {
                var x = i; // nested linq
                keyQuery = keyQuery.Where(entity => _keyProperties[x].GetValue(entity, null).Equals(keyValues[x]));
            }

            return keyQuery.SingleOrDefault();
        }

        public T Add(T item)
        {
            GenerateId(item);
            _data.Add(item);
            return item;
        }

        public T Remove(T item)
        {
            _data.Remove(item);
            return item;
        }

        public T Attach(T item)
        {
            _data.Add(item);
            return item;
        }

        public void Detach(T item)
        {
            _data.Remove(item);
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        public T Create()
        {
            return Activator.CreateInstance<T>();
        }

        public ObservableCollection<T> Local
        {
            get
            {
                return new ObservableCollection<T>(_data);
            }
        }

        public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }
    }

Hope this code is useful to someone else :)

Windows Identity Foundation : A SignInResponse message may only redirect within the current web application

November 14, 2011 Leave a comment

We have run into an issue in the WSFederationAuthenticationModule class within WIF.

The bug has been posted here on Microsoft connect.
http://connect.microsoft.com/site642/feedback/details/573589/wsfederationauthenticationelement-web-config-requires-trailing-slash-after-home-realm-otherwise-an-error-occurs

Basically the issue comes when the application requesting authentication from your STS sends a redirection url without a trailing slash. The slash cannot always be guaranteed as the return URI is created from the accessed application URI. Therefore if a user bookmarks the wrong URI or a link doesn’t have the trailing slash you will get the above exception message.

In trying to fix this problem I had a look through the WSFederationAuthenticationModule’s public event listings. None of the events that we could see occurred before the offending code in OnAuthenticateRequest().

The workaround listed on Microsoft Connect was quite bad as it forced us to check every single request in each RP to ensure that the trailing slash was in the URI, and if not redirect back to itself with the trailing slash.

I came up with a simpler solution to the problem which only needs a change to the STS itself. Since we couldn’t get into a public event before the offending code we added a HttpModule to the pipeline and ensured it ran before the WS modules.

This custom http module simply checked the ‘ru’ return URI field (within the wctx entry in the query string) for a trailing slash and if none was provided redirected back to itself with the trailing slash in the ‘ru’ field.

I’ve updated the Microsoft Connect website for the new work around.

Categories: Uncategorized

Getting back into it.

November 7, 2011 1 comment

Well I’ve been back from the holiday for a month or so. Been staying away from the blog for a while as I’ve had so much work to do since I got back that I need to prioritise.

I’m currently working on starting a new project in ASP.NET MVC 3 so you might see a few posts pop up about that. At the moment we are just going through the early stages of release planning and story estimation.

Stay tuned.

Categories: Uncategorized

Multi Tenant update

Hi,
I’ve received messages and I know a few of you are waiting for a good tutorial on multi tenancy.

I’ve been busy working as I’m going on a trip and will be away for the next 6 weeks. So it’s going to have to wait, we haven’t gone live with my changes I’ve made at work yet as they are not complete however in development it is working very nicely.

I’ll write up some posts when I get back. Basically my solution for the back-end involved:

  • MEF for drop-in client overrides of business logic in a services layer
  • EF 4.1 for POCO classes implementing an interface which allows for a hash set of key,value pair custom fields

This gives us a flexible back-end. As for the UI we have PHP, Webforms and MVC so that’s taking quite a bit of work to change. There are already great tutorials on multi tenancy MVC so I’d refer to those.

Looking forward to getting into it when I’m back.

Categories: Uncategorized

IIS 6 Session timeout issue (Web.Config cannot solve all timeout problems)

We were receiving calls from a client saying that they kept being kicked out of one of our systems quite regularly usually after 20-30 mins of inactivity.

We got to work and firstly checked the usual suspects, the web.config file sessionState timeout and the forms timeout for forms authentication. Both of those were set to > 2 hours so it couldn’t have been those.

I debugged on my local machine and found that everything was working fine and the session was being killed after the correct timeout stated in the web.config file. It seemed it was a server configuration issue. Now our situation is complex as we share authentication across multiple systems including legacy PHP code, So i tried all of those configs, to no avail.

At last eureka! It seems the actual application pool was being recycled after 20 mins of inactivity. This was an application pool being used by only 1 client so was not being hit as often as most. If your having similar problems check your application pool’s session time out values and application recycle values.. Ours was set to 20 mins of inactivity which I believe is the default.

Categories: Uncategorized Tags: ,
Follow

Get every new post delivered to your Inbox.