Turn WCF REST Protocol Exception into MVC ModelError

If you've found this blog posting, you've probably been banging your head against a wall trying to get those errors generated from your web service to appear in your MVC web site. You may have already discovered that while you can see the messages display in Fiddler, getting them to appear in your project takes a little bit of work.

First, l strongly recommend you check out the work already done on this subject by more dedicated bloggers than l:

http://kenneththorman.blogspot.com/2011/02/wcf-rest-exception-handling.html
http://www.olegsych.com/2008/07/simplifying-wcf-using-exceptions-as-faults/

http://www.robbagby.com/rest/effective-error-handling-with-wcf-rest/

My solution was built on Keneth Thorman's work, so read his posting, download and play with his zip project.

Where my solution differs from Keneth's example is that I'd like to use FluentValidation in my WCF RESTFul web service, use the ChannelFactory and pass coherent messages back to my MVC view. One would expect that to be easily done, right out of the box, but the ChannelFactory returns a ProtocolException which is not going to give you much love.

Let's assume that on your WCF service you've to a FluentValidation on your web site (fig #1), a method for rolling up all your validation errors (fig.#2), and a method that will invoke your validation (fig #3). For your web site, let's assume you're using the ChannelFactory and have an interface and a class (fig #4) which you've got communicating to your service, and everything seems to be working wonderfully, until you've discovered that you're not getting back the sort of error messages that are very useful to a user. The collection of validation errors you saw in Fiddler seem to be getting swallowed by the Protocol exception.

They're not exactly being swallowed, but they're not exactly at your fingertips either. To get the error messages to appear in your project, we need to do the following

  1. Serialize our error messages (fig #5)
  2. Trap the error where the ChannelFactory proxy invokes our service call (fig #6)
  3. Draw the information we need out of the error (courtesy of Keneth Thorman) (fig #7)
  4. Repackage that information into a custom error type (fig #8)
  5. Catch the custom error in our controller and inject our errors into the ModelState (fig #9)

Figure #1

  public class ShoppingCartValidator : AbstractValidator<ShoppingCart>
    {
       public ShoppingCartValidator()
        {
            RuleFor(r => r.CustomerId)
                .NotEmpty();

           RuleFor(r => r.Expires)
                .NotEmpty()
                .MustBeAValidSqlServerDateTime();                 

            RuleFor(r => r.TotalPrice)
                .NotEmpty()
                .LessThan(214748);

           RuleFor(r => r.ShoppingCartItems).SetCollectionValidator(new ShoppingCartItemValidator());
           
          }
    }
}

Figure #2

   public static void ValidateThrow<TValidator>(this TValidator validator, IValidatableEntity entity) where TValidator : IValidator
        {
            var validationResult = validator.Validate<TValidator>(entity);
            if (!validationResult.IsValid)
            {
                throw new WebFaultException<List<ValidationError>>(validationResult.ValidationErrors, System.Net.HttpStatusCode.BadRequest);
            }
        }

 FIgure #3

        public ShoppingCart Save(string id, ShoppingCart model)
        {
            if (model.Id.ToString() == id)
            {
                _cartValidator.ValidateThrow(OnBeforeSave(model));
                _repository.Save(model);
                return model;
            }

            throw new WebFaultException<string>("The input is invalid", System.Net.HttpStatusCode.BadRequest);
        }

Figure #4

 [ServiceContract]
    public interface IShoppingCartService
    {

        [OperationContract]
        [WebInvoke(BodyStyle = WebMessageBodyStyle.Bare,
           ResponseFormat = WebMessageFormat.Xml,
           UriTemplate = ShoppingCart.UriTemplate + UriTemplateHelper.IdTemplate)]
        void Save(string id, ShoppingCart entity);

 }

      [ServiceContract]
    public class ShoppingCartService : ClientBase<IShoppingCartService>, IShoppingCartService
    {

     public void Save(string id, ShoppingCart entity)
        {
            Channel.Save(id, entity);           
        }

}

Figure #5 Validation Error object on wcf service site.

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

namespace Model
{
    // Object that provides information about the invalidity of an entity.
    [DataContract]
    public class ValidationError
    {
        // Gets or sets member name associated with the validation result.
        [DataMember]
        public string Source { get; set; }

        // Gets or sets the error message for the validation result.
        [DataMember]
        public string Message { get; set; }
    }
}

Figure #6 We now trap our exception here as compared to #4

        public void Save(string id, ShoppingCart entity)
        {
            try
            {
                Channel.Save(id, entity);
            }
            catch (Exception exceptionThrownByRestWcfCall)
            {
                ExceptionHelper<ShoppingCart>.ProcessError(exceptionThrownByRestWcfCall);
            }
            finally
            {
                ((IDisposable)Channel).Dispose();
            }

        }

Figure #7 This is built very closely to KT's example, but as you can see we're building up a new custom error message, which we throw once we've finished building it up. Again, my work is built on his, so check out his project first...

namespace Helpers
{
    public static class ExceptionHelper<TServiceEntity> where TServiceEntity : class
    {
        public static void ProcessError(Exception exceptionThrownByRestWcfCall)
        {
            var errors = new List<ValidationError>();
            WcfRestExceptionHelper<TServiceEntity, List<ValidationError>>.HandleRestServiceError(
                   exceptionThrownByRestWcfCall,
                   serviceResult =>
                   {
                       HandleServiceResult(serviceResult);
                   },
                   serviceFault =>
                   {
                       foreach (var item in serviceFault)
                       {
                           errors.Add(new ValidationError { Message = item.Message, Source = item.Source });
                       }
                   },
                   exception =>
                   {
                       errors.Add(new ValidationError { Message = exception.ToString(), Source = exception.Source });                     
                   }
            );
            var wcfErr = new WcfFaultException();
            wcfErr.ValidationErrors.AddRange(errors);
            throw wcfErr;

        }
        private static void HandleServiceResult<T>(T serviceResult) where T : class// happy path
        {
            // a hook to do something, along the happy path...
        }
    }
}

 

    public static class WcfRestExceptionHelper<TServiceResult, TServiceFault> where TServiceFault : class
    {
        private static IDictionary<Type, DataContractSerializer> cachedSerializers = new Dictionary<Type, DataContractSerializer>();

        public static void HandleRestServiceError(Exception exception, Action<TServiceResult> serviceResultHandler, Action<TServiceFault> serviceFaultHandler = null, Action<Exception> exceptionHandler = null)
        {
            var serviceResultOrServiceFaultHandled = false;

            if (exception == null) throw new ArgumentNullException("exception");
            if (serviceResultHandler == null) throw new ArgumentNullException("serviceResultHandler");

            // REST uses the HTTP procol status codes to communicate errors that happens on the service side.
            // This means if we have a teller service and you need to supply username and password to login
            // and you do not supply the password, a possible scenario is that you get a 400 - Bad request.
            // However it is still possible that the expected type is returned so it would have been possible
            // to process the response - instead it will manifest as a ProtocolException on the client side.
            var protocolException = exception as ProtocolException;
            if (protocolException != null)
            {
                var webException = protocolException.InnerException as WebException;
                if (webException != null)
                {
                    var responseStream = webException.Response.GetResponseStream();
                    if (responseStream != null)
                    {
                        try
                        {
                            // Debugging code to be able to see the reponse in clear text
                            //SeeResponseAsClearText(responseStream);

                            // Try to deserialize the returned XML to the expected result type (TServiceResult)
                            var response = (TServiceResult) GetSerializer(typeof(TServiceResult)).ReadObject(responseStream);
                            serviceResultHandler(response);
                            serviceResultOrServiceFaultHandled = true;
                        }
                        catch (SerializationException serializationException)
                        {
                            // This happens if we try to deserialize the responseStream to type TServiceResult
                            // when an error occured on the service side. An service side error serialized object
                            // is not deserializable into a TServiceResult

                            // Reset responseStream to beginning and deserialize to a TServiceError instead
                            responseStream.Seek(0, SeekOrigin.Begin);

                            var serviceFault = (TServiceFault) GetSerializer(typeof(TServiceFault)).ReadObject(responseStream);

                            if (serviceFaultHandler != null && serviceFault != null)
                            {
                                serviceFaultHandler(serviceFault);
                                serviceResultOrServiceFaultHandled = true;
                            }
                            else if (serviceFaultHandler == null && serviceFault != null)
                            {
                                throw new WcfServiceException<TServiceFault>() { ServiceFault = serviceFault };
                            }
                        }
                    }
                }
            }

            // If we have not handled the serviceResult or the serviceFault then we have to pass it on to the exceptionHandler delegate
            if (!serviceResultOrServiceFaultHandled && exceptionHandler != null)
            {
                exceptionHandler(exception);
            }
            else if (!serviceResultOrServiceFaultHandled && exceptionHandler == null)
            {
                // Unable to handle and no exceptionHandler passed in throw exception to be handled at a higher level
                throw exception;
            }
        }

        /// <summary>
        /// Based on the knowledge of how the XmlSerializer work, I found it safest to explicitly implement my own caching mechanism.
        /// Just in case the DataContractSerializer has the same implementation. Please see below
        ///
        /// From MSDN:
        /// To increase performance, the XML serialization infrastructure dynamically generates assemblies to serialize and
        /// deserialize specified types. The infrastructure finds and reuses those assemblies. This behavior occurs only when
        /// using the following constructors:
        ///
        /// XmlSerializer.XmlSerializer(Type)
        /// XmlSerializer.XmlSerializer(Type, String)
        ///
        /// If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded,
        /// which results in a memory leak and poor performance. The easiest solution is to use one of the previously mentioned
        /// two constructors. Otherwise, you must cache the assemblies.
        ///
        /// Investigating exactly how the DataContractSerializer is working internally will be the subject of a future blog posting.
        /// </summary>
        /// <param name="classSpecificSerializer"></param>
        /// <returns></returns>
        private static DataContractSerializer GetSerializer(Type classSpecificSerializer)
        {
            if (!cachedSerializers.ContainsKey(classSpecificSerializer))
            {
                cachedSerializers.Add(classSpecificSerializer, new DataContractSerializer(classSpecificSerializer));
            }
            return cachedSerializers[classSpecificSerializer];
        }

        /// <summary>
        /// Debugging helper method in case there are problems with the deserialization
        /// </summary>
        /// <param name="responseStream"></param>
        private static void SeeResponseAsClearText(Stream responseStream)
        {
            var responseStreamLength = responseStream.Length;
            var buffer = new byte[responseStreamLength];
            var x = responseStream.Read(buffer, 0, Convert.ToInt32(responseStreamLength));
            var enc = new UTF8Encoding();
            var response = enc.GetString(buffer);
            Debug.WriteLine(response);
            responseStream.Seek(0, SeekOrigin.Begin);
        }
    }

Figure #8 We create an overloaded type of FaultException (although it could have just been exception)
namespace Helpers.Common
{
    public class WcfFaultException : FaultException
    {
        private List<ValidationError> validationErrors = new List<ValidationError>();
        public WcfFaultException()
        {
        }

        public List<ValidationError> ValidationErrors
        {
            get
            {
                return this.validationErrors;
            }
        }
    }
}

 

Figure #9 the model.

   [HttpPost]
        public virtual ActionResult Add(ShoppingCartForm model)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    _modelSvc.Save(_modelMapper.Map(model));
                    return RedirectToAction("Index");   
                }                   
                catch (WcfFaultException f)  // trap our error
                {
                    foreach(var err in f.ValidationErrors) // convert too validation error.
                    {
                        ModelState.AddModelError(err.Source, err.Message);
                    }                   
                }          
            }

            return View(model);
        }

How TiVo is messing up customer support.


Ok,

 So I've gotten a TiVo and overall, I'm happy, but there have been issues and I suspect I've a defective unit. - Now the nice folks after many service calls were happy to swap it out, and to ensure continuity of service, they sent me a new unit (after a $109 deposit).  That was yesterday. Today, when we go to watch a little TV, and wait for our replacement unit to arrive we find our TiVo service has been suspended. WTF? They have an exchange program, but your unit your waiting to exchange is as dead as a doornail until the replacement arrives. How hard is it to keep the old unit active for an extra week?

Here is the exchange w/Tivo below...

You are currently number 1 in the queue. We apologize for the delay. We will assign you to an agent as soon as one is available.The average amount of time a customer has to wait is 00:13.
 Kaylene (Listening)
 Kaylene: Thank you for contacting TiVo! My name is Kaylene. So that I may better assist you, are you an existing customer?
 james Fleming: yes I am, but I'm now having second thoughts about being one
 

 Kaylene: Thank you for verifying your information. How may I assist you today James?
 james Fleming: I've been having issues w/a tivo box & I'm getting a replacement sent out to me (after paying an additional deposit) and now my current unit is no longer activated
 Kaylene: I can help you today!
 Kaylene: When we process an exchange we do transfer over the service to the replacement box so it is active and ready to go when you receive it.
 james Fleming: which is to say you also make my current box worthless until such time I receive a new box?!?!?
 Kaylene: I apologize that your original box was deactivated so we could activate your replacement box.
 james Fleming: Why on Earth would I bother to pay in advance for a new box if you were going to kill my existing box.
 Kaylene: What features are you needing to use on your current box?
 james Fleming: I need to be able to access my netflix subscription (if I'm lucky enough to have it work without rebooting)
 Kaylene: Can I have you verify the TiVo Service Number of your TiVo box please?
 james Fleming: 7460011906979b4
 Kaylene: We have your current box temporary service but not all features are available with temporary service as it is not paid for service.
 Kaylene: If you like I can transfer your service back to your current box for now. Then once you receive the new box you will have to call in and have the service transferred back to the new box.
 james Fleming: Not paid for? Let's see> one tivo box + 3 year service plan + monthly service + $109 deposit on a second box = what?
 Kaylene: Would you like me to transfer your service back to your current box?
 james Fleming: Yes - that would be helpful
 Kaylene: All you will need to do is contact us again once you receive the new box so we can transfer it back.
 Kaylene: I have put your service back on TiVo box 7460011906979b4.
 james Fleming: What would also be helpful is your firm informing me to how you'd be cutting service in the interim.
 james Fleming: Again - I opted to pay to have a second box delivered BEFORE returning the box I have - thus trying to have a continuity of service..
 Kaylene: This is not something we normally do so it is important when you contact us to transfer the service back to the new box when you receive it that you reference this case number: 110622-006089.
 Kaylene: I apologize about the inconvenience. You may need  force a few connections for the box to recognize the service again.
 james Fleming: If it's not something you normally do than WHY would you have a $109 fee and a term for the service.
 james Fleming: I am not mad at you, but your company is not impressing me and I'm blogging about this experience
 Kaylene: Again I apologize about the inconvenience but you should be good to go now. Is there anything else I can help you with today?
 james Fleming: so I need to go through the re-actviate process or is that somethign you do
 Kaylene: When you receive the new TiVo box you need to contact us so we can transfer the service to the new box for you.
 james Fleming: sure
 Kaylene: Is there anything else I can help you with today James?
 james Fleming: Nope - please email this transcript to me
 Kaylene: I apologize but we do not have the ability to e-mail you a copy of this transcript. You can view it online at  http://www.tivo.com when you sign into your account or you can copy and paste it now to save it.
 Kaylene: Thank you for contacting TiVo today. Your reference number for our conversation is 110622-006089. You can save this for your records, and if necessary, provide this to a later agent to pull up what we discussed. There will be a brief satisfaction survey emailed to you. We would appreciate any feedback on your TiVo Chat Support experience today.
 Kaylene: Thank you for using TiVo Chat and have a great day James! Good-bye.
 Kaylene has disconnected.

Cut the cable resources & articles

A great article breaking down the 'actual cost' of one man's attempt to recreate his viewing habits without cable.

http://hdguru.com/setting-your-hdtv-free-cutting-the-cablesatellite-cord/4527/

Thoughtful, but slightly off in the math. For example comparing going OTA + TiVo to cable is only for the first year, where all the up front costs are, but not comparing year two, when the savings are obviously dramatic. Still the approach can help you quantify your own decision - 'gee my Bravo shows aren't on Hulu' - does that become a deal breaker? For me those decisions become worth considering if the few shows I can't get by going off cable are worth $100/month.  The obvious answer here is Bethany Ever After and Bill Maher are not worth $100 a month. Also, the author failes to mention what is gained in addition to money saved.

Go TiVo and you can transfer shows to your phone, iPad, PC, etc.And if you ditch the cable bill, you'll be able to afford a new tablet every year for what you're tossing to the cable company.

Adios Fios or How I Cut the Cable

I just wanted to chronicle my eventual path to saying adios to my monthly TV subscription via FIOS.

If you've found this article, you're no doubt looking to save money by having one fewer hand in your pocket every month. My wife and I had some discussions on this subject at length and after reviewing places where we could save some coin was in the cable bill, or in our case, the FIOS bill.

We have FIOS Triple Play which is NOT the $99.99 advertised, but $145/month since we've gone way passed the promo period. Switching to the FIOS Double Play (Internet/Phone) will bring our bill down to $90/month.  While $145 seems like a pretty good value, when you add in the DVR rental, additional subscriptions for channels worth watching and On Demand content, the average monthly bill is 209.61, which means I’ve spent $3563.36 over the last 17 months. That is real money.

We are a family of 4, so I couldn’t exactly have a Fight Club moment where I just blow up my apartment and live a simple life in an abandon building on Paper Street. We love watching TV together as a family, but I don’t want to give up family vacations to do it, so this blog posting will show you the path I took to better spend $209.61/month.

When looking at transitioning my family the first thing I did was cancel the HBO and other premium channels. The rational here is that I’d fill up the DVR with shows I think the family would enjoy, only to get voted down on movie night for the latest chick-flick that was On Demand. After a while, I realized I didn’t miss HBO (although Game of Thrones did look like a winner).

So, I examined our viewing habits and realized we watched a combination of movies, sit-coms and reality shows on Bravo (the wife, not me). The later was a problem, because Bravo is only available on line or on cable. That was going to be a problem, and maybe even a deal breaker, so I knew it made sense to see if I could transition our viewing habits away from cable before I cancelled the TV portion of FIOS. For the Netflix appliance, I picked the Sony BDPS780 from Crutchfield. It’s a good unit and at $249, I am quite pleased with it. Why I chose this over a lesser expensive unit is that it or even a Roku is because it comes with an optical audio output and that mattered to me. Roku seemed to be a great choice too, but we don’t have a Blu-ray player and now that we’ve got Netflix, disks are only $2 more per month.

As it turns out, once we got Netflix, my wife and daughters got hooked on a tween-ager soap opera called Make it or Break it. So long Housewife’s of New York! Now that I’ve gotten the family to transition their viewing habits, it came time to say Adios Fios.

First hurdle is the OTA shows – For this I opted for the Winegard HD7000R VHF/UHF/FM DTV TV Antenna (HD-7000R). This got great reviews and seems to perform better than similar Channel Master antenna’s from what I’ve read. This only advice I can give when selecting an antenna is to choose a model that is sized appropriately for the stations you’re trying to reach – bigger isn’t always better. Fortunately for me the stations I want are all in a cluster within one degree of each other, so I don’t have to deal with a rotor and I can avoid using an omnidirectional antenna. The downside to an omnidirectional antenna is you can get more ‘ghosting’. Go to www.fcc.gov/mb/engineering/maps  see location of the stations of interest to you. The difference in price between cheap antenna’s and the top of the line isn’t enough to even worry about. All in, the antenna, mast, wiring, brackets, etc. put me out $156. Where I live, I could I have gotten away with rabbit ears, but I’m also looking to split that signal with other TV’s so the expense wasn’t worthy of a lot of consideration.

The installation was easy. Granted I did have to pull out an extension ladder, but if you’re not found of heights, you can go with an in-the-attic model. The outside installation requires a grounding block, so make sure you get one, and follow all the safety precautions your installation requires.

The last step for me is to select the DVR for OTA HD. There are really only three choices. You can either get a Channel Master OTA DVR, for around $300. The upside here is there is no subscription. You can get a TiVo which costs $100 + a $20/month fee – certainly better than what I’m paying FIOS, but that replaces one hand in my pocket for a smaller hand. TiVo also will let you buy a lifetime (of the device) subscription for $400 or I can rig up my server with an OTA tuner, Windows Home Media Center and somehow get my server wired into my TV and controlled remotely from the next room. While I’m geek enough to make that happen, the last thing I want to do is be the tech-support for the household, so I’m not considering that option.

At first, I was only going to go with the Channel Master, but from what I’ve read and seen, the menu is way inferior to the TiVo menu What TiVo has going for it. The TiVo additionally records 45 hours of HD vs. the 30 of the Channel Master. The TiVo also has an optional QWERTY keyboard remote, which would come in handy when searching Hulu, etc. I’ll be honest though, the $499.99 lifetime service has me choking.  That works out to about 25 months of dues before I’ve broken even.

The factory-renewed TiVo Premier (with $500 screwing) actually works out to $180 more than the Channel Master and when I put it that way, it’s a better purchase, and with the current 6 months of free Hulu, that works out to only being $140 more. The part that was causing me the most pain was that I’d already dropped $240 for the Blu-ray, but as luck would have it, my new Sony gave up the ghost as I was writing this blog posting, so the good folks at Crutchfield said they’d let me return it, and as it turns out, the $240 is what the re-furbished TiVo with 3 year warrantee and QWERTY remote cost.  I’ll hold back on the lifetime commitment until I’ve actually tried the unit. Even still, I’ll be going from $100/month on cable to $28 with TiVo + Netflix. My entire outlay for the switch is  $156 + $240 = $396, which will take me 6 months to recover.

 

 

.

 

wcf http 504: Working on a mystery

Ok,

 So you're here because you've been trying to solve the mystery of why you're getting a 504 error. If you've made it to this lonely corner of the Internet, then the advice you're getting from other bloggers isn't the answer you are after. It wasn't the answer I needed either, so once I did solve my problem, I thought I'd share the answer with you.

For starters, if by some miracle, you landed here first you may not already know that the 504 error is NOT coming from IIS or Casini, that response code is coming from Fiddler.

HTTP/1.1 504 Fiddler - Receive Failure
Content-Type: text/html
Connection: close
Timestamp: 09:43:05.193

ReadResponse() failed: The server did not return a response for this request.      

The take away here is Fiddler won't help you with the diagnosis and any further digging in that direction is a red herring.

Assuming you've dug around a bit, you may have arrived at posts which suggest you may be getting the error because you're trying to hump too much data over the wire, and have an urgent need to employ an anti-pattern: due to a special case: http://delphimike.blogspot.com/2010/01/error-504-in-wcfnet-35.html

Or perhaps you're experiencing wonky behavior using WCF-CustomIsolated Adapter on Windows Server 2008 64bit environment, in which case the rather fly MVP Dwight Goins' advice is what you need.

http://dgoins.wordpress.com/2009/12/18/64bit-wcf-custom-isolated-%E2%80%93-rest-%E2%80%93-%E2%80%9C504%E2%80%9D-response/

For me, none of that was helpful.

I could perform a get on a single record  http://localhost:8783/Criterion/Skip(0)/Take(1)

but I couldn't get more than one record in my collection as in:

 http://localhost:8783/Criterion/Skip(0)/Take(2)

I didn't have a big payload, or a large number of objects (as you can see by the size of one record below)

- - A-1B f5abd850-ec52-401a-8bac-bcea22c74138 .biological/legal mother This item refers to the supervisor’s evaluation of the caseworker’s ability to involve the biological/legal mother in the permanency planning process. 75d8ecb7-91df-475f-aa17-26367aeb8b21 false true Admin account 2010-01-06T17:58:24.88 1.20 764a2333-f445-4793-b54d-1c3084116daa

So while I was able to retrieve one record without a hitch (thus the record above) I wasn't able to return multiple records. I confirmed I could get each record individually, (Skip(1)/Take(1))so it stood to reason the problem wasn't with the data at all, so I suspected a serialization error.

The first step to resolving this was to enable WCF Tracing. Instructions on how to set it up are here: http://msdn.microsoft.com/en-us/library/ms733025.aspx. The tracing log led me to the solution.

The use of type 'Application.Survey.Model.Criterion' as a get-only collection is not supported with NetDataContractSerializer.  Consider marking the type with the CollectionDataContractAttribute attribute or the SerializableAttribute attribute or adding a setter to the property.

So I was wrong (but close!). The problem was a deserializing issue in trying to recreate my read only collection. http://msdn.microsoft.com/en-us/library/aa347850.aspx#Y1455

So looking at my underlying model, I saw I did have a read only collection. Adding a setter was all it took.

        public virtual ICollection<string> GoverningResponses
        {
            get
            {
                if (!string.IsNullOrEmpty(GoverningResponse))
                {
                    return GoverningResponse.Split(';');
                }
                else
                    return null;
            }         
        }

Hope this helps. If it does, post a comment.

Executable specifications: pondering BDD tools and automation.

I've been trying to push the client where I work into a more test driven approach, but when you're dealing with state agencies, if you want to get people nervous, just say the word "Change". They want 'impovements', just not 'change'.  In many respects, they're not unlike the person who wants to lose weight, but not change their eating habits. Now that I'm moving on to greener pastures, I can again resume my deep dive into NSpec, NBehave and the rest, however since I've also been digging into templating tools, my perspective is that it's possible to populate a template that will write out the human readable specifcations and stub out the tests. (more on this later)0.

I have begun analyzing different BDD tools and approaches and while some might consider what makes sense for them to be the ONLY WAY IT SHOULD BE DONE. I've got a more pragmatit approach to the whole arguement. If you can pick an approach that is saving you time, improve communication with the stakeholders, streamline the development process and above all eliminate redundancy, you've got the right tool.

I had been excited by the notion of an executable specification: that is a document which could be easily consumed by developers and managers and stakeholders alike. The holy grail of an automated executable specification may be unatainable and even unadvisable for a two of reasons. The first being that it may unduly constrain an agile process - the design process gets driven in multiple directions (from the client who has evolving needs, from the PM who has deadlines and resource constraints and from the dev who needs to deliver and knows quite well that the map is not the territory).

My instincts tell me that while it is probably easier to create the prose from the BDD tool vs parsing out praragraphs using regular expressions, a third way would be to build out a lightweight tool which could be used in a meeting where the variables were either injected into an input form with a backing database and then the preliminary prose and code templates get generated. This tool might help assure that the requirements are traceable.

An example of BDD with StoryQ
Story is Data Safety
In order to keep my data safe as a user, I want all credit card numbers to be encrypted within the scenario of submitting shopping cart. Given I have typed my credit card number into the checkout page, when I click the 'Buy' button, and the browser posts my credit card number over the Internet, then the form should be posted over https.
 

Which maps to
Story is Data Safety
In order to Keep my data safe
As a User
I want All credit card numbers to be encrypted
With scenario of submitting shopping cart
Given I have typed my credit card number into the checkout page
When I click the 'Buy' button
And the browser posts my credit card number over the Internet
Then the form should be posted over https
 

When we examine this in a more structured form, we discover our variables: story, justification, role, modtivation, etc:
Story is <story>
In order to <justification>
As a <role>
I want <motivation>
With <scenario>
Given <context>
When <invoking action, "qualifier">
And <reaction>
Then <assertion clause , result>
(assertion clause) <noun, adverb, verb>

Which can easily injected back into prose or code:

As prose
In order to <justification> as a <role>, I want <motivation> within the <scenario>. Given <context>, when <invoking action, "qualifier">, and <reaction>, then <assertion clause , result>.
 

As StoryQ:
new Story("<story>").Tag("sprint 1")
        .InOrderTo("<justification>")
         .AsA("<role>")
        .IWant("<motivation>")
        .WithScenario("<scenario>")
        .Given(<context>)
        .When(<invoking action, "qualifier">)
        .And(<assertion clause , result>)
        .Then(<assertion clause , result>)
        .ExecuteWithReport(MethodBase.GetCurrentMethod());
 

Which a templating tool could generate as * 
using StoryQ;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace StoryQ.Demo
 {
 [TestClass]
public class DemoTest
{ [TestMethod] public void PassingExample()
{
    new Story("Data Safety").Tag("sprint 1")
        .InOrderTo("Keep my data safe")
         .AsA("User")
        .IWant("All credit card numbers to be encrypted")
        .WithScenario("submitting shopping cart")
        .Given(IHaveTypedMyCreditCardNumberIntoTheCheckoutPage)
        .When(IClickThe_Button, "Buy")
        .And(TheBrowserPostsMyCreditCardNumberOverTheInternet)
        .Then(TheForm_BePostedOverHttps, true)
        .ExecuteWithReport(MethodBase.GetCurrentMethod());
}

* StoryQ tests requires far more code than what is shown here, but you get the idea. If you've thoughts on the subject, I'd like to here them.
 

Spec Flow Example

Story is Bowling
Feature:: Score Calculation
In order to know my performance as a player I want the system to calculate my total score

Scenario: Gutter Game: Given a new bowling game, when all my balls are landing in the gutter, then my total score should be 0
Scenario all strikes: Given a new bowling game, when all my balls are strikes, then my total score should be 300

Which maps to

Spec Flow
Feature:: Score Calculation
In order to know my performance
As a player
I want the system to calculate my total score

Scenario: Gutter Game
Given a new bowling game
When all my balls are landing in the gutter
Then my total score should be 0

Scenario all strikes
Given a new bowling game
When all my balls are strikes
Then my total score should be 300

When we examine this in a more structured form, we discover our variables: story, justification, role, modtivation, etc:

Spec Flow
Feature::<story>
In order to <justification>
As a <role>
I want <motivation>

Scenario: <scenario>
Given <context>
When <invoking action, "qualifier">
Then <assertion clause, result>

You can see in both cases that they are using the domain specific language of BDD with only minor differences. StoryQ uses (but does not require) an AND clause.


http://specflow.org/specflow/getting-started.aspx
http://storyq.codeplex.com/

injecting model variables into javaScript the 'right way',

 

I had started to write this post a while back, but never polished it up enough to make it worthy of print, however it does offer some insight into a clean way of injecting model variables into javaScript the 'right way'*, that is not putting <%= %> tags in your script. I've learned quite a lot since I started this blog posting, so I'm NOT recommending this as the only approach, or best approach, etc. Just as a neater way to inject your model into your client side script.

An example of using JQuery with ASP.NET/MVC with Rick Stahl''s Web Utility to populate child controls:

 Let's assume you want to use JQuery where you wish to have client side data sent back to a controller without a full page post back.

 * I did say 'reight way' in quotes - ;-)

For the project:

You need to inject a client-side JavaScript. While you can spin one up by hand, why not use go green and recycle?  Rich Stahl's site: http://www.west-wind.com has a variety of tools, some of which are free. Download http://www.west-wind.com/westwindwebtoolkit/. What we're interested in here is the Web Utility Support Classes

 Download the utility and add references for Westwind.Utilities and Westwind.Web to your project. You'll also need to add ScriptVariableInjectionHelper.cs to your helpers folder.

 ScriptVariableInjectionHelper provides an easy way for server code to publish strings into client script code. This object basically provides a mechanism for adding key value pairs and embedding  those values into an object that is hosted on the client.

 

For the Master Page:

Before you start injecting scripts willy-nilly all over your pages, it is better to inject all of your client-side scripts into the header along with your other JavaScript includes.. If you are using a master page, this becomes very easy. Beneath where you put all of your other site-wide include tags, and above a content placeholder for page-level scripts, I also add a simple test for controller scripts I inject into the ViewData and a test to verify it is there before you write it out.

 

<script type="text/javascript" language="javascript" src="<%=Url.Content("~/Content/Scripts/jquery-1.3.2.min.js")%>"></script>

  <% if(ViewData.Contains("clientScript")){

        Response.Write(ViewData["clientScript"].ToString());

  }%>

<asp:ContentPlaceHolder ID="Scripts"   runat="server" />

</head>

 

For the Controller:

Now that we have a way to inject our script and a target to inject it into, we need to simply send it down for manipulation.

 

From your ActionMethod we add a call to a new InjectClientScript method...

public virtual ActionResult Edit(Guid id)

  {

      Criterion model = _CriterionSvc.GetCriterion(id);

      InjectClientScript(model);

      return View(model);

}

 

private void InjectClientScript(object model)

{

    // instantiate the utility (view is the name of the JavaScript object that will appear in the View

    ScriptVariables scriptVars = new ScriptVariables("view");

    // add our ProfileCommon data (user context)

    scriptVars.Add("Profile", profile.Preferences);

    // add our model being injected

    if (model != null)

      scriptVars.Add("Model", model);

    // call the westwind utility and inject into viewdata

    ViewData["clientScript"] = scriptVars.GetClientScript(true);

 }

 

For the view:

The output is injected into a variable called view, which in turn contains our Controller objects we injected (Profile and Model). Note Model contains objects of its own. In this example, CriterionType is one such object. You can configure the westwind utility to traverse child objects or not.

 
<script type="text/javascript"> 
var view = {
 Profile: {"CountryId":0,"StateId":21,"DistrictId":6108},
 Model: {"CriterionTypeId":7,"CriterionType":{"DomainId":1,"Domain":null,"Description":null,"Code":"Instructional Design Skills","Id":7,"Guid":{"Length":36},"ModifiedDate":new Date(-62135578800000),"ModifiedBy":null},"Description":null,"Code":"Teacher provides relevant examples and demonstrations to illustrate concepts and skills.\r\n","Id":31,"Guid":{"Length":36},"ModifiedDate":new Date(1245546360000),"ModifiedBy":"Admin"}
};
</script>

 

You now simply need to access your javascript variables.Below is a simple example of accessing those variables and sending them to a jquery function that would populate a dropdown via ajax.

 

$.PopulateDropdown("CriterionTypeId", "/CriterionType/GetCriterionTypeCollection/" + view.Model.CriterionType.DomainId, view.Model.CriterionTypeId);


 

your jquery plug in would look something like this:

 

(function () {
    $.issPopulateDropdown = function (control, query, selected, emptyMsg) {
        control = "#" + control;
        $(control + ' > option').remove(); // remove any existing options
        $(control).show();
        $('#emptyMsg').remove(); // remove any existing message
        $.ajax({
            type: "GET",
            contentType: "application/json; charset=utf-8",
            url: query,
            dataType: "json",
            success: function (data) {
                if (data.length > 0) {
                    var options = '<option id="0">Select</option>';
                    for (p in data) {
                        var item = data[p];
                        options += "<option value='" + item.Id + "'>" + item.Key + "</option>";
                    }
                    $(control).html(options);
                    $(control).val(selected);
                    $('#emptyMsg').remove();
                }
                else {
                    alert('there are not any existing options');
                    $(control).after('<div id="emptyMsg">' + emptyMsg + '</div>');
                    $(control).hide();
                }
            } // end success          
        });  // end ajax
    } // end issPopulateDropdown

));

Code Generation with T4 toolbox Part 1: The elevator pitch

This is the first in a several part posting about T4-Toolbox. More to follow

You landed here because you're interested in code generation, working faster, generating uniform code, forestalling corpal tunnel syndrome, etc. But perhaps, you need convincing it's worth the effort 

What's the difference between this class:
    public partial class Evaluation : KeyedObject
    {
        public virtual string Description { get; set; }
        public virtual Guid EvaluationTypeId { get; set; }
        public virtual decimal Bias { get; set; }
        public virtual decimal Correlation { get; set; }
        public virtual decimal Version { get; set; }
        public virtual decimal Release { get; set; }
        public virtual EvaluationType EvaluationType { get; set; }
        public virtual LazyList<ParticipantEvaluation> ParticipantEvaluations { get; set; }
    }

and this class:
    public partial class Evaluation : KeyedObject
    {
        [DisplayName("Description")]
        [Required(ErrorMessage ="Description is required")]
        [StringLength(500, ErrorMessage ="Description max length is 500" )]
        public virtual string Description { get; set; }

        [DisplayName("Evaluation Type")]
        [Required(ErrorMessage ="Evaluation Type is required")]
        public virtual System.Guid EvaluationTypeId { get; set; }
  
        [DisplayName("Bias")]
        [Required(ErrorMessage ="Bias is required")]
        public virtual decimal Bias { get; set; }
   
        [DisplayName("Correlation")]
        [Required(ErrorMessage ="Correlation is required")]
        public virtual decimal Correlation { get; set; }
   
        [DisplayName("Version")]
        [Required(ErrorMessage ="Version is required")]
        public virtual decimal Version { get; set; }
   
        [DisplayName("Release")]
        [Required(ErrorMessage ="Release is required")]
        public virtual decimal Release { get; set; }
       [DisplayName("Evaluation Type")]
        public virtual EvaluationType EvaluationType { get; set;}
        [DisplayName("Participant Evaluations")]
        public virtual LazyList<ParticipantEvaluation> ParticipantEvaluations { get; set;}
}


On the surface you can see that both are describing the same domain object, and that one version has implemented DataAnnotations. What you can't see here is that one of these classes was generated dynamically and in virtually under a second using the T4 Toolbox.  I know that may not seem all that exciting, even if you've already guessed that it was the version that has the DataAnnotations is the one generated from the template.


Let's look at something a bit more complex.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Survey.Core.Domain.Model;

namespace Survey.Core.Domain
{
    public class EvaluationRepository : IEvaluationRepository
    {

        private Survey.Core.Repository.SqlDataContext _context = new Survey.Core.Repository.SqlDataContext();

        public EvaluationRepository() { }

        public EvaluationRepository(Survey.Core.Repository.SqlDataContext context)
        {
            _context = context;
        }
       

        public IQueryable<Evaluation> GetIQueryableCollection()
        {
            ICriterionRepository criteriaRep = new CriterionRepository(_context);
            return from entity in _context.Evaluations
                   let criteria = criteriaRep.GetIQueryableCollection().Where(x => x.EvaluationId == entity.Id)
                   select new Evaluation()
                   {  
                    Code = entity.Code,
                    Description = entity.Description,                   
                    Id = entity.Id ,
                    EvaluationTypeId = entity.EvaluationTypeId,
                    IsEditable = entity.IsEditable,
                    Release = entity.Release,
                    ReplacesEvaluationId = entity.ReplacesEvaluationId,
                    Version = entity.Version,
                    Criteria = new LazyList<Criterion>(criteria),
                    EvaluationType = new EvaluationType{ Id= entity.EvaluationType.Id, Code=entity.EvaluationType.Code},
                    ModifiedDate = entity.ModifiedDate,
                    ModifiedBy = entity.ModifiedBy,
                   
                   };
        }
       

        public void Save(Evaluation model)
        {
            if (model != null)
            {
                Survey.Core.Repository.SqlDataContext dataCtx = new Survey.Core.Repository.SqlDataContext();
                using (dataCtx)
                {
                    Survey.Core.Repository.Evaluation entity;
                    try
                    {
                        entity = (from e in dataCtx.Evaluations
                                  where e.Id == model.Id
                                  select e).SingleOrDefault() ?? new Survey.Core.Repository.Evaluation();
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                    //// synch it
                    entity = Sync(entity, model);

                    if (entity.Id == Guid.Empty)
                        dataCtx.Evaluations.InsertOnSubmit(entity);
                    try
                    {
                        dataCtx.SubmitChanges();
                        model.Id = entity.Id;
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                }
            }
        }

        public void Delete(Evaluation evaluation)
        {
            throw new NotImplementedException();
        }

        public Evaluation[] GetAll()
        {
            Evaluation[] list = GetIQueryableCollection().ToArray<Evaluation>();
            return list;
        }

        public Evaluation GetById(Guid id)
        {
            return GetIQueryableCollection().Where(x => x.Id == id).FirstOrDefault();
        }
       
        public Evaluation GetByKey(string key)
        {
            return GetIQueryableCollection().Where(x => x.Code == key).FirstOrDefault();
        }

        static private Repository.Evaluation Sync(Survey.Core.Repository.Evaluation entity, Evaluation model)
        {
            entity.Code = model.Code;
            entity.Description = model.Description;
            entity.EvaluationTypeId = model.EvaluationTypeId;
            entity.IsEditable = model.IsEditable;
            entity.ModifiedBy = model.ModifiedBy;           
            entity.ModifiedDate = DateTime.Now;
            entity.Release = model.Release;
            entity.ReplacesEvaluationId = model.ReplacesEvaluationId;
            entity.Version = model.Version;
            return entity;
        }
    }
}
 

As compared to:
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Survey.Core;
using Survey.Core.Domain.Model;

namespace Survey.Core.Domain.Model
{

     public interface IEvaluationRepository : IKeyedRepository<Evaluation>
    {
        new void Save(Evaluation entity);
        new void  Delete(Evaluation entity);
        new Evaluation[] GetAll();
        new Evaluation GetById(Guid id);
        IQueryable<Evaluation> GetIQueryableCollection();
        Survey.Core.Repository.MetrixContext  GetDataContext();
    }
 
    public class EvaluationRepository : IEvaluationRepository
    {
        private Survey.Core.Repository.MetrixContext _context = new  Survey.Core.Repository.MetrixContext();
       
        public EvaluationRepository (){}
       
        public EvaluationRepository(Survey.Core.Repository.MetrixContext context)
        {
            _context = context;
        }
       
        public  IQueryable<Evaluation> GetIQueryableCollection()
        {
            return from entity in _context.Evaluations
               select new Evaluation()
               {
                    Id = entity.Id,
                    Key = entity.Key,
                    EvaluationTypeId = entity.EvaluationTypeId,
                    Description = entity.Description,
                    Bias = entity.Bias,
                    Correlation = entity.Correlation,
                    Version = entity.Version,
                    Release = entity.Release,
                    IsEditable = entity.IsEditable,
                    ReplacesEvaluationId = entity.ReplacesEvaluationId,
                    Elaboration = entity.Elaboration,
                    ModifiedDate = entity.ModifiedDate,
                    ModifiedBy = entity.ModifiedBy,
                 };   
         }
       
        public void Save(Evaluation model)
        {
            if (model != null)
            {
                Survey.Core.Repository.MetrixContext dataCtx = new Survey.Core.Repository.MetrixContext();
                using (dataCtx)
                {
                    Survey.Core.Repository.Evaluation entity;
       
                    entity = (from e in dataCtx.Evaluations
                              where e.Id == model.Id
                              select e).SingleOrDefault() ?? new Survey.Core.Repository.Evaluation();
       
                    //// synch it
                    entity = Sync(entity, model);
       
                    if (entity.Id == Guid.Empty)
                        dataCtx.Evaluations.InsertOnSubmit(entity);
                    try
                    {
                        dataCtx.SubmitChanges();
                        model.Id = entity.Id;
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                }
            }
        }
        public void Delete(Evaluation model)
        {
            if (model != null)
                {
                    Survey.Core.Repository.MetrixContext dataCtx = new Survey.Core.Repository.MetrixContext(); // we create a separate data context for saves, as they are disposed after the insert.
                    using (dataCtx)
                    {
                         Survey.Core.Repository.Evaluation entity;
                        try
                        {
                            if (model.Id != Guid.Empty)
                            {
                                entity = (from e in dataCtx.Evaluations
                                             where e.Id == model.Id
                                             select e).SingleOrDefault();
                                if (entity != null)
                                {
                                    dataCtx.Evaluations.DeleteOnSubmit(entity);
                                    dataCtx.SubmitChanges();
                                }
                            }
                        }
                        catch (Exception)
                        {
                            throw;
                        }
                    }
                }
        }
        public Evaluation[] GetAll()
        {
            return GetIQueryableCollection().ToArray<Evaluation>();
        }
       
        public Evaluation GetById(Guid id)
        {
            return GetIQueryableCollection().Where(x => x.Id == id).FirstOrDefault();
        }
       
        public Evaluation GetByKey(string key)
        {
            return GetIQueryableCollection().Where(x => x.Key == key).FirstOrDefault();
        }
         
        public  Survey.Core.Repository.Evaluation Sync(Survey.Core.Repository.Evaluation entity, Evaluation model)
        {
                entity.Id = model.Id;
                entity.Key = model.Key;
                entity.EvaluationTypeId = model.EvaluationTypeId;
                entity.Description = model.Description;
                entity.Bias = model.Bias;
                entity.Correlation = model.Correlation;
                entity.Version = model.Version;
                entity.Release = model.Release;
                entity.IsEditable = model.IsEditable;
                entity.ReplacesEvaluationId = model.ReplacesEvaluationId;
                entity.Elaboration = model.Elaboration;
                entity.ModifiedDate = model.ModifiedDate;
                entity.ModifiedBy = model.ModifiedBy;
            return entity;
        }
       
        public Survey.Core.Repository.MetrixContext GetDataContext()
        {
             return _context;
        }
    }
}

One was crafted by hand, the second drawn from a template. If you look close enough, you'll be able to tell which one was hand coded because it's the one that contains an unimplemented excpetion!  This is what excites me about code generation: the ability to build the software you want to build in less time.  In this article I'm going to show you how I leveraged the open source T4 Toolbox to build out complex project files. 
 

The code for this project is now up on codeplex http://t4tarantino.codeplex.com/
 

Microsoft.SqlServer.Management.Sdk.Sfc.ISfcValidate is defined in an assembly that is not referenced

I've taken to learning T4 templates using tutorials from Oleg Sych, and I hit a wall right away, and so I decided that since the solution, while simple took a great deal of searching, I thought I'd toss together how I resolved it, in case it can help someone else.

Compiling transformation: The type 'Microsoft.SqlServer.Management.Sdk.Sfc.ISfcValidate' is defined in an assembly that is not referenced. You must add a reference to assembly 'Microsoft.SqlServer.Management.Sdk.Sfc, Version=10.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91'. 
 

I couldn't find a lot of info on this one, but eventually I got it resolved and final credit goes out to Sebastian Iglesias who pointed out you may need to add the following additional reference.

 


 

<#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>

Dropdown list ReDux

I had been kicking around the idea of writing an article discussing the various approaches in MVC to populating the ubiqutous dropdown list box, when at lucnch today, I read that the prolific K. Scott Allen had beat me to it. Rather than rehash what he wrote, I'm not free to skip the part discussing the basics of adding a SelectList to a ViewModel and discuss the various other approaches.

What's the ViewModel?

That is a class representing a DTO of your entity that is more readily consumed by a view. It may contain validation, UI hints as well as an Enumberable for our SelectList.

While this works quite well, there are a few downsides with this approach, the largest being that you need to re-hydrate the list if, when you post back to the server an error occurs. K. Scott Allen mention how some folks might deal with this by having an inbound & outbound view. To my thnking this is a HUGE violation of DRY and is undesirable.

Another approach is to re-hydrate the dropdown when ever you error trap and need to resend the view to the server:

catch(Exception)
{
model.Albums = _repository.FindAllAlbums().ToSelectItems(selectedId);
}

There's nothing really wrong with this, so long as you remember to do it on all of your errors & validation failures.

What I've opted to do most recently, was to create a helper class for all of my work horse dropdowns. I've removed most of the methods from my actual class so this example isn't muddled, but you should be able to see how it works fairly easily.

The helper has an enum set to the type of list the view is interested in. The Get method uses a switch to return a call to the list of the type desired. Additional properties manage filtering, if required and adjusting the selected index. The GetAll methods used by the ObjectFactory pull on my Service Layer where the values are cached, so overall, the performance is quite good.


  public class SelectListHelper
    {      
        private object _htmlAttributes = null;
        private Category[] _categories = null;     
        private Response[] _response = null;
        private SelectList _selectList = null;
        private string _label = null;
        private bool _showLabel = true;

        public enum ListOfType{
            Category,
            Response
        }
        public enum  FilterOfType
        {          
            Category,          
            Response
        }
       
        public object FilterId { get; set; }
        public Enum FilterResults { get; set; }
        public SelectList SelectList {
            get {
                switch (ReturnsListOf)
                {
                    case ListOfType.Category:
                        return GetCategoriesSelectList();                   
                    case ListOfType.Response:
                        return GetResponseSelectList();
                    default:
                        return _selectList;
                }
            }
            set {_selectList = value; }
        }

        public bool ShowLabel
        {
            get { return _showLabel; }
            set { _showLabel = value; }        
        }

        private string GetLabelName()
        {
          
                switch (ReturnsListOf)
                {
                    case ListOfType.Category:
                        return  "Domain";                  
                    case ListOfType.Response:
                        return "Response";
                    default:
                        return string.Empty;
                }
           }
        private Category[] Categories
        {
            get
            {
                if (_categories == null)
                {
                    _categories = ObjectFactory.GetInstance().GetAll().ToArray();
}
return _categories;
}
set { _categories = value; }
}

private Response[] Response
{
get
{
if (_response == null)
_response = ObjectFactory.GetInstance().GetAll().ToArray();
return _response;
}
set { _response = value; }
}

private SelectList GetCategoriesSelectList()
{
return new SelectList(Categories, "Id", "Key", SelectedValue);
}
private SelectList GetResponseSelectList()
{
return new SelectList(Response, "Id", "Key", SelectedValue);
}

public object SelectedValue { get; set; }

public object HtmlAttributes
{
get
{
if (IsPostBack)
{
_htmlAttributes = new { onchange = "this.form.submit()" };
}
return _htmlAttributes;
}
set { _htmlAttributes = value; }
} /// /// Flag for which default collection will hydrate the SelectList /// public ListOfType ReturnsListOf { get; set; } /// /// If set to true, all other html attributes are overwritten /// public bool IsPostBack { get; set; } public string Label { get { return string.IsNullOrEmpty(_label) ? GetLabelName() : _label; } set { _label = value; } } /// /// Readonly name used by the dropdown the ReturnsListOfValue + Id /// public string Name { get { return ReturnsListOf + "Id"; } }

The Shared View is pretty straight forward: It renders the dropdown with an optional label & form, in case I wish the dropdown to post back to the controller.

 


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>    
<%if (Model.ShowLabel)
{ %>
<%= Html.Label(Model.Label) %>
<%} %> <%if (Model.IsPostBack) { using (Html.BeginForm()) { %> <%= Html.DropDownList(Model.Name, (SelectList)Model.SelectList, "(Select)", Model.HtmlAttributes) %> <% } } else { %> <%= Html.DropDownList(Model.Name, (SelectList)Model.SelectList, "(Select)", Model.HtmlAttributes)%> <% } %>

What is in the View is even easier to digest


Html.RenderPartial(MVC.Shared.Views._SelectList, new SelectListHelper { ReturnsListOf = SelectListHelper.ListOfType.EvaluationType, SelectedValue = ViewData["EvaluationTypeId"] , IsPostBack=true});

Twitter