Wednesday, February 6, 2013

CSV Download with ASP.NET Web API

This is something I played around with and had to cobble some code snippets together. Forcing a download is a bit different with Web API than it is with regular ASP.NET MVC. The example that I am working with is as follows: A user is presented data in a grid. Two buttons on the grid give the user options to filter data which will bring back a new data set to the grid. Roughly, what we need to do is grab the filter terms from the UI, go back to the server, query the repository, get the data we need in CSV format, force the browser to execute the file download. The trick is that in order for the browser to force a download, we need to post a form back to the controller. This example will be using the JObject. If you are not familiar with it, it is in the Newtonsoft.Json.Linq dll. It offers a really nice way to work with JSON. I like it for the flexibility where you find yourself in a one-off situation where you need a data container and dont' want to use a view model, since it will only be used once.

To start, the JavaScript. This isn't the entire script but it's the necessary part:
var values = {
   selectedFormat: null,
   selectedGenre: null
}

// pull values from UI
values.selectedGenre = $('#genre').val();
values.selectedFormat = $('#format').val();

// clear hidden form
$('#hidden').html('');

// post form back to Web API on the fly
$('<form>').attr({
   method: 'POST',
   id: 'hidden',
   action: 'http://localhost:12345/api/GridApi/ExportData'
}).appendTo('body');

$('<input>').attr({
   type: 'hidden',
   id: 'genre',
   name: 'genre',
   value: values.selectedGenre
}).appendTo('#hidden');

$('<input>').attr({
   type: 'hidden',
   if: 'format',
   name: 'format',
   value: values.selectedFormat
});

$('#hidden').submit();

One thing to point out, the first time this code runs, the hidden element ($('#hidden')) does not exist. It first comes into existence when we create the form and give it the id of hidden. However, after the page loads, if the user request another search, the line $('#hidden').html('') will clear the form.

Now for the Web API code:
public class GridApiController : ApiController
{
   [HttpPost]
   public HttpResponseMessage ExportData(JObject values)
   {
      string genre = values.GetValue("genre").ToString();
      string format = values.GetValue("format").ToString();
      SearchParams searchParams = new SearchParams() { SelectedFormat = format, SelectedGenre = genre };
      List viewModels = Search(searchParams);
      string result = ConvertListToCsv(viewModels);
      
      HttpResponseMessage message = new HttpResponseMessage(HttpStatusCode.OK);
      message.Content = new StringContent(result);
      message.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
      message.Content.Headers.ContentDisposition = new ContentDispositonHeaderValue("attachment");
      message.Content.Headers.ContentDisposition.FileName = "Test.csv");

      return message;
   }

   public List<MusicItemViewModel> Search(SearchParams values)
   {
      // code omitted, just filter logic against repository
   }

   private string ConvertListToCsv<T>(List<T> list)
   {
      if (list == null || list.Count == 0)
      {
         throw new HttpResponseException(HttpStatusCode.NotFound);
      }

      Type t = typeof(T);
      string newLine = Environment.NewLine;

      object obj = Activator.CreateInstance(t);
      PropertyInfo[] props = obj.GetType().GetProperties();
      byte[] carriageReturnBytes = System.Text.Encoding.UTF8.GetBytes("\r");

      string text;
      using (MemoryStream ms = new MemoryStream())
      using (StreamReader sr = new StreamReader(ms))
      {
         foreach (PropertyInfo pi in props)
         {
            byte[] data = System.Text.Encoding.UTF8.GetBytes(pi.Name.ToString() + ",");
            ms.Write(data, 0, data.Length);
         }

         ms.Write(carriageReturnBytes, 0, carriageReturnBytes.Length);

         foreach (T item in list)
         {
            foreach(PropertyInfo pi in props)
            {
               string write =
                  Convert.ToString(item.GetType().GetProperty(pi.Name).GetValue(item, null)).Replace(',', '') + ',';

               byte[] data = System.Text.Encoding.UTF8.GetBytes(write);
               ms.Write(data, 0, data.Length);
            }

            byte[] writeNewLine = System.Text.Encoding.UTF8.GetBytes(Environment.NewLine);
            ms.Write(writeNewLine, 0, writeNewLine.Length);
         }

         ms.Position = 0;
         text = sr.ReadToEnd();
         return text;
      }
   }
}

A short note. If you are separating your scripts for a modular purpose, separation of concerns, etc, here is an additional approach. If you have a client side data service to call back to the server, it's only a minor change....
// the original script abbreviated
// post form back to Web API on the fly
$('<form>').attr({
   method: 'POST',
   id: 'hidden',
   action: 'http://localhost:12345/api/GridApi/ExportData'
}).appendTo('body');

$('<input>').attr({
   type: 'hidden',
   id: 'genre',
   name: 'genre',
   value: values.selectedGenre
}).appendTo('#hidden');

$('<input>').attr({
   type: 'hidden',
   if: 'format',
   name: 'format',
   value: values.selectedFormat
});

// commented out
//$('#hidden').submit();

// assign hidden form to variable
var hiddenForm = $('#hidden');

// call data service
dataService.ExportGridData(hiddenForm);


// dataService.js
var dataService = function () {

   var exportGridData = function (hiddenForm) {
      // wrap hidden form in jQuery so we can call submit
      $(hiddenForm).submit();
   };

   return {
      ExportGridData: exportGridData
   }
}

The form we created on the fly contains the URL to call on the controller so everything will work as it should.

Monday, February 4, 2013

Entity Framework Fluent API Mappings

Entity Framework Code First allows you to take control of your database schema. You can do this two ways, decorating your POCO's with attributes or using the fluent API. This post will show how to define relationships using the fluent API.

One to Many With Foreign Key:
public class Chats
{
   // Chats has one to many with ChatMessage
   public virtual ICollection<ChatMessage> ChatMessages { get; set; }

   // Chats has one to many with EventGroup
   public virtual ICollection<EventGroup> EventGroups { get; set; }

   // Chats is on the many side of one to many with Users
   // Set up navigation by providing Id and the POCO to reference back
   public virtual int UserId { get; set; }
   public virtual Users User { get; set; }

   // Chats is on the many side of one to many with ChatType
   // Set up navigation by providing Id and the POCO to reference back
   public virtual int ChatTypeId { get; set; }
   public virtual ChatType ChatTypeEntity { get; set; }
}

public ChatsConfig()
{
   // Chats is on the many side of one to many with Users
   HasRequired(c => c.User)
      .WithMany(u => u.Chats)
      .HasForeignKey(c => c.UserId);

   // Chats is on the many side of one to many with ChatType
   HasRequired(c => c.ChatTypeEntity)
      .WithMany(cte => cte.Chats)
      .HasForeignKey(c => c.ChatTypeId);
}

The Chats POCO has two ICollection properties and two navigation properties back to the referenced POCO's. We configure the one side which is represented by the navigation property and the corresponding Id. In this case, UserId and User, ChatTypeId and ChatTypeEntity.

 
 


Another One to Many With Foreign Key:

public class Artist
{
   public virtual int AritstId { get; set; }
   public virtual string AritstName { get; set; }
   public virtual string Country { get; set; }

   public virtual Genre Genre { get; set; }
   public virtual ICollection<Title> Titles { get; set; }
}

public ArtistConfig()
{
   // Artist has many Titles, required
   HasMany(a => a.Titles).WithRequired(t => t.Artist);
}

This approach is different from above. This time we are mapping the side that contains the ICollection<T>, as we can see the ICollection<Title> Titles property in the Artist class. Another difference is that we are using WithRequired here. This simply means that both ends of the relationship must exist in order for it to be valid. This does have a drawback. If you really want to delete an item from the collection, this set up will not work. You would have to delete the entire relationship. To get around that, you could make a composite key which will allow deleting collection items.

 


Composite Primary Key:
public class AboutUsImage
{
   public virtual long DealerId { get; set; }
   public virtual short ImageOrder { get; set; }
}

public AboutUsImageConfig()
{
   HasKey(aui => new { aui.DealerId, aui.ImageOrder } );
}

This one is pretty straightforward. You just take the two columns you need for the composite key and put them together in an anonymous object.

 


Composite Primary Key With Foreign Key:
public class Permission
{
   public virtual long UserId { get; set; }
   public virtual string PermissionType { get; set; }
   public virtual string AccessLevel { get; set; }

   public virtual User User { get; set; }
}

public PermissionConfig()
{
   // composite key, UserId and PermissionType
   // a User has many Permissions so UserId is also FK
   HasKey(p => new { p.UserId, p.PermissionType } )
      .HasRequired(p => p.User).WithMany(p => p.Permissions);

The composite key is the same as above. This foreign key case is a bit odd. Note that the foreign key is not explicitly defined here. EF inferred it. If you want to explicitly define, it would look like this:
   HasKey(p => new { p.UserId, p.PermissionType })
      .HasRequired(p => p.User).Withmany(p => p.Permissions)
      .HasForeignKey(p => p.UserId);
 
 

One to One:
public class User
{
   public virtual int UserId { get; set; }
   public virtual string Name { get; set; }
   pubilc virtual Address Address { get; set; }
}

public class Address
{
   public virtual int AddressId { get; set; }
   public virtual string Street { get; set; }
   public virtual string City { get; set; }
   public virtual string Zip { get; set; }
}

public UserConfig()
{
   HasOptional(u => u.Address).WithRequired();
}

To specify the one to one, you use HasOptional and WithRequired. There is no need to use any Id properties. If you do not see the relationship itself, create a diagram in SQL Server Management Studio.

 


Many to Many Relationship:
public class UserProfile
{
   public int UserId { get; set; }
   public string UserName { get; set; }
   public virtual ICollection Roles { get; set; }
}

public class Role
{
   public int RoleId { get; set; }
   public string RoleName { get; set; }
   public virtual ICollection UserProfiles { get; set; }
}

Right away we can see a relationship between UserProfile and Role. They each contain an ICollection of each other, of course, this is an indication of a many to many relationship. It doesn't matter which entity we choose, either will get the job done. We'll use UserProfile. Define the properties as normal for the POCO. But when dealing with the ICollection, this is where we set up the many to many:

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class UserProfileConfig : EntityTypeConfiguration
{
   public UserProfileConfig()
   {
      HasKey(up => up.UserId);
      Property(up => up.UserId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
      Property(up => up.UserName).HasColumnName("UserName").HasColumnType("nvarchar").HasMaxLength(128).IsOptional();

      HasMany(up => up.Roles)
         .WithMany(r => r.UserProfiles)
         .Map(usersinroles =>
         {
            usersinroles.MapLeftKey("UserId");
            usersinroles.MapRightKey("RoleId");
            usersinroles.ToTable("webpages_UsersInRoles");
         });

      ToTable("UserProfile");
   }
}


Saturday, February 2, 2013

Integrating SimpleMembership with Entity Framework

Integrating SimpleMembership with Entity Framework is quite nice. Instead of having two databases, collapse it all into one and have alll the info you need in one place. I'll be using a MVC 4 Internet application in VS 2012. I'll be putting all the POCO's and support classes in the Models folder for simplicity. First order of business, in the Filters folder, delete the InitializeSimpleMembershipAttribute C# file, as this will not be needed. We need to remove two classes from the AccountModels C# file. Open up AccountModels in the Models folder and delete the UserContext and UserProfile classes. For SimpleMembership we will have to create the four tables necessary for it: webpages_Membership, webpages_OAuthMembership, webpages_Roles, and webpages_UsersInRoles. Additionally, we will also add a UserProfile table and a dummy Person table. I like to add some support files for the EntityConfig classes and this requires StructureMap. Grab StructureMap from NuGet, it doesn't have to be the StructureMap.MVC package, just the plain StructureMap package. In the Models folder, add a class file, IEntityConfiguration:
using System.Data.Entity.ModelConfiguration.Configuration;

public interface IEntityConfiguration
{
   void AddConfiguration(Configuration registrar);
}

Add a class file, ContextConfiguration:
using System.Collections.Generic;
using StructureMap;

public class ContextConfiguration
{
   public IEnumerable Configurations
   {
      get { return ObjectFactory.GetAllInstances(); }
   }
}

Just to get it out of the way quick, let's take care of bootstrapping StructureMap. Open global.asax.cs, in the Application_Start method, right above the call to AreaRegistration.RegisterAllAreas();, add SetStructureMap(); Now let's create that method:
private void SetStructureMap()
{
   ObjectFactory.Initialize(x =>
                               {
                                  x.Scan(scan =>
                                               {
                                                  scan.TheCallingAssembly();
                                                  scan.WithDefaultConventions();
                                                  scan.AddAllTypesOf();
                                               });
                               });
}

All the POCO's:
public class Membership
{
   public int UserId { get; set; }
   public DateTime? CreateDate { get; set; }
   public string ConfirmationToken { get; set; }
   public bool? IsConfirmed { get; set; }
   public DateTime? LastPasswordFailureDate { get; set; }
   public int PasswordFailuresSinceLastSuccess { get; set; }
   public string Password { get; set; }
   public DateTime? PasswordChangedDate { get; set; }
   public string PasswordSalt { get; set; }
   public string PasswordVerificationToken { get; set; }
   public DateTime? PasswordVerificationTokenExpirationDate { get; set; }
}

public class OAuthMembership
{
   public string Provider { get; set; }
   public string ProviderUserId { get; set; }
   public int UserId { get; set; }
}

public class Role
{
   public int RoleId { get; set; }
   public string RoleName { get; set; }
   public virtual ICollection UserProfiles { get; set; }
}

public class UserProfile
{
   public int UserId { get; set; }
   public string UserName { get; set; }
   public virtual ICollection Roles { get; set; }
}

public class Person
{
   public int PersonId { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

The EntityConfig classes will contain the validation and other schema information:
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class MembershipConfig : EntityTypeConfiguration<Membership>, IEntityConfiguration
{
   public MembershipConfig()
   {
      HasKey(m => m.UserId);
      Property(m => m.UserId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
      Property(m => m.ConfirmationToken).HasColumnName("ConfirmationToken").HasColumnType("nvarchar").HasMaxLength(128);
      Property(m => m.IsConfirmed).HasColumnName("IsConfirmed").HasColumnType("bit").IsOptional();
      Property(m => m.LastPasswordFailureDate).HasColumnName("LastPasswordFailureDate").HasColumnType("datetime").IsOptional();
      Property(m => m.PasswordFailuresSinceLastSuccess).HasColumnName("PasswordFailuresSinceLastSuccess").HasColumnType("int").IsRequired();
      Property(m => m.Password).HasColumnName("Password").HasColumnType("nvarchar").HasMaxLength(128).IsRequired();
      Property(m => m.PasswordChangedDate).HasColumnName("PasswordChangedDate").HasColumnType("datetime").IsOptional();
      Property(m => m.PasswordSalt).HasColumnName("PasswordSalt").HasColumnType("nvarchar").HasMaxLength(128).IsRequired();
      Property(m => m.PasswordVerificationToken).HasColumnName("PasswordVerificationToken").HasColumnType("nvarchar").HasMaxLength(128);
      Property(m => m.PasswordVerificationTokenExpirationDate).HasColumnName("PasswordVerificationTokenExpirationDate").HasColumnType("datetime").IsOptional();
   
      ToTable("webpages_Membership");
   }

   public void AddConfiguration(ConfigurationRegistrar registrar)
   {
      registrar.Add(this);
   }
}

using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class OAuthMembershipConfig : EntityTypeConfiguration<OAuthMembership>, IEntityConfiguration
{
   public OAuthMembershipConfig()
   {
      HasKey(o => new { o.Provider, o.ProviderUserId });
      Property(o => o.Provider).HasColumnName("Provider").HasColumnType("nvarchar").HasMaxLength(30).IsRequired();
      Property(o => o.ProviderUserId).HasColumnName("ProviderUserId").HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
      Property(o => o.UserId).HasColumnName("UserId").HasColumnType("int").IsRequired();

      ToTable("webpages_OAuthMembership");
   }

   public void AddConfiguration(ConfigurationRegistrar registrar)
   {
      registrar.Add(this);
   }
}

using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class RoleConfig : EntityTypeConfiguration<Role>, IEntityConfiguration
{
   public RoleConfig()
   {
      HasKey(r => r.RoleId);
      Property(r => r.RoleId).HasColumnName("RoleId").HasColumnType("int").IsRequired();
      Property(r => r.RoleName).HasColumnName("RoleName").HasColumnType("nvarchar").HasMaxLength(256).IsRequired();

      ToTable("webpages_Roles");
   }

   public void AddConfiguration(ConfigurationRegistrar registrar)
   {
      registrar.Add(this);
   }
}

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class UserProfileConfig : EntityTypeConfiguration<UserProfile>, IEntityConfiguration
{
   public UserProfileConfig()
   {
      HasKey(up => up.UserId);
      Property(up => up.UserId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
      Property(up => up.UserName).HasColumnName("UserName").HasColumnType("nvarchar").HasMaxLength(128).IsOptional();

      HasMany(up => up.Roles)
         .WithMany(r => r.UserProfiles)
         .Map(usersinroles =>
         {
            usersinroles.MapLeftKey("UserId");
            usersinroles.MapRightKey("RoleId");
            usersinroles.ToTable("webpages_UsersInRoles");
         });

      ToTable("UserProfile");
   }

   public void AddConfiguration(ConfigurationRegistrar registrar)
   {
      registrar.Add(this);
   }
}

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;

public class PersonConfig : EntityTypeConfiguration<Person>, IEntityConfiguration
{
   public PersonConfig()
   {
      HasKey(p => p.PersonId);
      Property(p => p.PersonId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
      Property(p => p.FirstName).HasColumnName("FirstName").HasColumnType("nvarchar").HasMaxLength(128).IsRequired();
      Property(p => p.LastName).HasColumnName("LastName").HasColumnType("nvarchar").HasMaxLength(128).IsRequired();
   }

   public void AddConfiguration(ConfigurationRegistrar registrar)
   {
      registrar.Add(this);
   }
}

Of course we will be needing a DbContext class, add a file named MembershipContext:
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

public class MembershipContext : DbContext
{
   public MembershipContext()
      : base("name=DefaultConnection")
   {
   }

   public DbSet Membership { get; set; }
   public DbSet OAuthMemberships { get; set; }
   public DbSet Roles { get; set; }
   public DbSet UserProfiles { get; set; }
   public DbSet People { get; set; }

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
   {
      modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
      Database.SetInitializer(new MembershipTestInitializer());

      ContextConfiguration ctxConfiguration = new ContextConfiguration();

      foreach (IEntityConfiguration configuration in ctxConfiguration.Configurations)
      {
         configuration.AddConfiguration(modelBuilder.Configurations);
      }
   }
}

Two things to note: We are passing in a connection string on the constructor of the MembershipContext class and we are setting up some initialization in a class called MembershipTestInitializer. Regarding the connection string, if you are using VS2010 and SQL Server 2008, you can just omit the constructor altogether. In that case, by default, EF will use SQL Server Express. If you are using VS2012 and you have SQL Server 2010 and 2012 installed, you have some options. If you omit the constructor, EF will use SQL Server Express. Or, there is a default connection set in web.config. That is what we are doing. If you open web.config you will see the default connection in the connectionStrings section. You will also see that it is using the new LocalDb. LocalDb adds some jargon in the connection string that will not give us the exact database name we want. You can strip that out, for example:
<connectionStrings>
   <add name="DefaultConnection"
   connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=MembershipContext;
                     Integrated Security=SSPI; AttachDBFilename=|DataDirectory|\MembershipContext.mdf"
                     providerName="System.Data.SqlClient" />
</connectionStrings>

MembershipTestInitializer will contain seed membership info. This class will provide the bridge to bring SimpleMembership into our POCO's. After that, everything will be in one db:
using System.Data.Entity;
using System.Linq;
using System.Web.Security;
using WebMatrix.WebData;

public class MembershipTestInitializer : DropCreateDatabaseIfModelChanges
{
   protected override void Seed(MembershipContext context)
   {
      SeedMembership();
   }

   private void SeedMembership()
   {
      WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);

      SimpleRoleProvider roles = (SimpleRoleProvider) Roles.Provider;
      SimpleMembershipProvider membership = (SimpleMembershipProvider) System.Web.Security.Membership.Provider;

      if (!roles.RoleExists("Admin"))
      {
         roles.CreateRole("Admin");
      }
      if (membership.GetUser("sheldon", false) == null)
      {
         membership.CreateUserAndAccount("sheldon", "Password01");
      }
      if (!roles.GetRolesForUser("sheldon").Contains("Admin"))
      {
         roles.AddUsersToRoles(new string[] { "sheldon" }, new[] { "admin" } );
      }
   }
}

The call to WebSecurity.InitializeDatabaseConnection came from the InitializeSimpleMembershipAttribute C# file we deleted earlier. That is what originally kicks off the SimpleMembership db creation. In that method we are passing the connection string, the name of the table for UserProfile, the UserId, and the UserName. All of those line up nicely with the UserProfile POCO.
At this point, if we run the application, nothing will happen. We need to perform some sort of action to create the database. Initially, you may think, ok let's go and create a user. That will not work at this point because the line of code, WebSecutiry.InitializeDatabaseConnection, in the SeedMembership method has not run. The MembershipContext class has not been called, which kicks all this off.
To rectify that, simply go into the Index method of the HomeController and create a new Person. This should be enough to create the database:
public ActionResult Index()
{
   MembershipContext context = new MembershipContext();
   context.People.Add(new Person()
   {
      FirstName = "Sheldon",
      LastName = "Cooper"
   });

   return View();
}

Let's go have a look at the database:
 

And there we have it, all the SimpleMembership tables in our MembershipContext database.
EF FTW, eat it Tater.

ASP.NET MVC, JSONP, jQuery UI

For this post, we're going to dive into JSONP with MVC. The aim is to return content from another site and display it in a modal dialog. We will need two separate web projects running at the same time to see this in action. Environment is VS2012, ASP.NET MVC 4.

We'll start with the client site. Just spin up a new MVC 4 project, call it Client. Open up the Index view for the HomeController and wipe everything out. This, will be the view:

<input id="txtBox1" type="text" />
<button id="getData">Get Data</button>
<div id="modal"></div>

@section scripts {

}

Add a new JavaScript files to the Scripts folder, name it Index.js. We'll just be setting up the modal dialog and the AJAX call:
$(document).ready(function() {
   $('#modal').dialog({
      title: "Bazinga",
      modal: true,
      autoOpen: false,
      height: 300,
      width: 500
   });

   $('#getData').click(function(e) {
      $.ajax({
         type: 'GET',
         dataType: 'jsonp',
         url: 'fill this out later,
         success: function(data) {
            $('#modal').html(data.data);
            $('#modal').dialog("open");
         },
         error: function(jqXHR, textStatus, errorThrown) {
            alert(textStatus);
            alert(errorThrown);
         }
      });
   });
});

Note the dataType on the AJAX call, jsonp. We will come back and fill out the URL once we set up the other site. If you want, you can use the AJAX $.getJSON method instead, as such:
$.getJSON('http://localhost:12345/Home/Index2?jsoncallback=?', function(data) {
   $('#modal').html(data.data);
   $('#modal').dialog("open");
});

To wrap up the Client site, open up BundleConfig in the App_Start folder. We will create a bundle for the scripts we need. Put this right at the top of the RegisterBundles method:
public static void RegisterBundles(BundleCollection bundles)
{
   bundles.Add(new ScriptBundle("~/bundles/app").Include(
               "~/Scripts/jquery-{version}.js",
               "~/Scripts/jquery-ui-{version}.js",
               "~/Scripts/Index.js"));
}

Jump over to _Layout.cshtml and just change the jQuery bundle to app...
@Scripts.Render("~/bundles/app")
Time to fire up another web project, call this one Widget. To make this work, we will need a JsonpResult. Also, we will be rendering our content from a view as a string. This is a pretty slick technique, and we have Rick Strahl to thank. Take some time and go check out his post to get all the details on it: Rendering ASP.NET MVC Views to String

First, let's take care of the JsonpResult. I added a folder called Extensions, added a new C# file called JsonpResult:
using System;
using System.Web;
using System.Web.Mvc;

public class JsonpResult : JsonResult
{
   public override void ExecuteResult(ControllerContext context)
   {
      if (context == null)
      {
         throw new ArgumentNullException();
      }

      HttpRequestBase request = context.HttpContext.Request;
      HttpResponseBase response = context.HttpContext.Response;
      string jsoncallback = (context.RouteData.Values["jsoncallback"] as string) ?? request["jsoncallback"];

      if (!String.IsNullOrEmpty(jsoncallback))
      {
         if (String.IsNullOrEmpty(base.ContentType))
         {
            base.ContentType = "application/x-javascript";
         }

         response.Write(String.Format("{0}(", jsoncallback));
      }

      base.ExecuteResult(context);

      if (!String.IsNullOrEmpty(jsoncallback))
      {
         response.Write(")");
      }
   }
}

The important part here is the jsoncallback string. We look in Values of RouteData for a jsoncallback string, if we find one, we return, if not, we look in the request. Then we write the content type out. You can see this in Fiddler, let's have a looky look....
 
Add another C# file to the Extensions folder, called ViewRenderer:
using System;
using System.Web.Mvc;

public class ViewRenderer
{
   protected ControllerContext Context { get; set; }

   public ViewRenderer(ControllerContext controllerContext)
   {
      Context = controllerContext;
   }

   public static string RenderPartialView(string viewPath, ControllerContext controllerContext)
   {
      ViewRenderer renderer = new ViewRenderer(controllerContext);
      return renderer.RenderPartialView(viewPath);
   }

   public string RenderPartialView(string viewPath)
   {
      return RenderViewToStringInternal(viewPath, true);
   }

   protected string RenderViewToStringInternal(string viewPath, bool partial = false)
   {
      ViewEngineResult viewEngineResult = null;

      if (partial)
      {
         viewEngineResult = ViewEngines.Engines.FindPartialView(Context, viewPath);
      }
      else
      {
         viewEngineResult = ViewEngines.Engines.FindView(Context, viewPath, null);
      }

      if (viewEngineResult == null)
      {
         throw new FileNotFoundException("The view could not be found");
      }

      IView view = viewEngineResult.View;
      string result = null;

      using (StringWriter sw = new StringWriter())
      {
         ViewContext ctx = new ViewContext(Context, view, Context.Controller.ViewData, Context.Controller.TempData, sw);
         view.Render(ctx, sw);
         result = sw.ToString();
      }

      return result;
   }
}

Now to set up the content that will be returned to the client. In the Views folder, add a folder named Tmpl. In the Tmpl folder, add a view named Widget.cshtml. This is just going to be a simple example of sending down some text and JavaScript. Of course you have to be careful, scripting attacks, etc. Anywho, the Widget.cshtml file:
<p>Remote HTML loaded via JSONP.</p>
<br /><br />
<input type="button" value="Press Me!" onclick="Message()" />

<script type="text/javascript">
   function Message() {
      alert("Bazinga punk!");
   }
</script>

The final step is setting up an ActionResult in the HomeController. All we need to do is pass the path of our widget and the controller context to the ViewRenderer, which will return a string. We use this string as the data for our JsonpResult. The GetWidget ActionResult method in the HomeController:
using Widget.Extensions;

[HttpGet]
public ActionResult GetWidget()
{
   string result = ViewRenderer.RenderPartialView("~/View/Tmpl/Widget.cshtml", ControllerContext);
   return new JsonpResult()
              {
                 Data = new { data = result },
                 JsonRequestBehavior = JsonRequestBehavior.AllowGet
              };
}

I almost forgot. Run the Widget project. We need to grab the URL and put it in the AJAX call in the Client project. All you will need to change is the port number for the localhost. The completed AJAX call should look as such:
$('#getData').click(function(e) {
   $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: 'http://localhost:61691/Home/GetWidget?jsoncallback=?',
      success: function(data) {
         $('#modal').html(data.data);
         $('#modal').dialog("open");
      },
      error: function(jqXHR, textStatus, errorThrown) {
         alert(textStatus);
         alert(errorThrown);
      }
   });
});

You might be wondering, what the heck is up with that URL? Yes, yes. In order for cross-domain to work, you are required to add the "?jsoncallback=?" bit, that is the key. If you omit that, the call will not work.
Now to test it out. Fire up the Widget project, it will just appear as the default view since we didn't change any of that. Then fire up the Client project upon running....

 
Before we click the Get Data button, let's set a breakpoint in the Widget project. If everything works, we will hit it.

 
Click Get Data and...
 
Success! After stepping through the rest of that method, we should see the modal dialog pop up on the client....
 
The finale...

Friday, February 1, 2013

Shadowbox & ASP.NET

Shadowbox makes it very easy to load full pages in a model dialogue. However things can get a little complicated if you have to pass data between pages in ASP.NET. While this posting illustrates a solution, it is probably not the most elegant. This example was done with VS 2010 and ASP.NET 4.

First thing to do is add Shadowbox to the project. You can find it on NuGet so it is very simple. Then, add the script and css references in Site.Master in the head section:

<head runat="server">
   <title></title>
   <link href="~/Styles/Site.css" rel="stylesheet" type="text/css" />
   <script src="Scripts/jquery-1.4.1.js" type="text/javascript"></script>
   <script src="Scripts/Shadowbox/js/shadowbox.js" type="text/javascript"></script>
   <link href="Scripts/ShadowBox/css/shadowbox.css" rel="stylesheet" type="text/css" />
   <asp:ContentPlaceHolder ID="HeadContent" runat="server"></asp:ContentPlaceholder>
</head>
 
Basically the scenario is, input fields are displayed on a page with a search capability. When the "Find" hyperlink is clicked, it will launch Shadowbox and display another page. We will simulate finding a contact. On the parent page, there will be some labels, text boxes, hyperlinks, and hidden fields. Remove all code from Default.aspax and drop this in:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" >

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent"></asp:Content>
<asp:ContentID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">


<div id="container"
   <asp:Label runat="server" ID="lblPrimaryContact">Primary Contact</asp:Label>
   <asp:TextBox runat="server" ID="txtPrimaryContact"&gt/</asp:TextBox>
   <asp:HyperLink runat="server" ID="hlnkPrimaryFind" rel="shadowbox">Find</asp:HyperLink>
   <input type="hidden" id="primaryContact" value="" />
   <br />
   <asp:Label runat="server" ID="lblSecondaryContact">Primary Contact</asp:Label>
   <asp:TextBox runat="server" ID="txtSecondaryContact"&gt/</asp:TextBox>
   <asp:HyperLink runat="server" ID="hlnkSecondaryFind" rel="shadowbox">Find</asp:HyperLink>
   <input type="hidden" id="secondaryContact" value="" />
   <br />
   <input type="hidden" id="stateClickManager" value="" />
   <br />
   <br />
   <br />
   <br />
   <asp:Label runat="server" ID="hiddenLabel1"></asp:Label>
   <input type="hidden" id="hdnPrimary" value="" />
   <br />
   <asp:Label runat="server" ID="hiddenLabel2"></asp:Label>
   <input type="hidden" id="hdnSecondary" value="" />
</div>

</asp:Content>
 
Wiring up Shadowbox is simple. All you need to do is set the rel attribute value to shadowbox on a hyperlink and configure the links in the code behind. Notice the html hidden field with the id of stateClickManager. This hidden field will hold the value of which hyperlink was clicked so we can determine which text box in the parent should be filled from the search on the child page. Let's take care of the code behind quick, Default.aspx.cs:
public partial class _Default : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!Page.IsPostBack)
      {
         hlnkPrimaryFind.NavigateUrl = "~/FindContact.aspx";
         hlnkSecondaryFind.NavigateUrl = "~/FindContact.aspx";
      }
   }
}

To keep things simple, we'll drop JavaScript in the page right above the div id container:
<script type="text/javascript">
   function setUp() {
      // get reference to primary contact textbox
      var primaryContactTextbox = "<%=txtPrimaryContact.ClientID %>";
      // get reference to secondary contact textbox
      var secondaryContactTextbox = "<%=txtSecondaryContact.ClientID %>";
  
      // get reference to asp.net label
      var label = "<%=hiddenLabel1.ClientID %>";
      var label2 = "<%=hiddenLabel2.ClientID %>";

      // get reference to html hidden field
      var primaryHidden = document.getElementById('primaryContact');
      // assign primaryContactTextbox to primaryHidden value prop
      primaryHidden.value = primaryContactTextbox;
      // get refernce to html hidden field
      var hdnPrimary= document.getElementById('hdnPrimary');
      // hdnPrimary value prop contains asp.net label
      hdnPrimary.value = label;

      // get reference to html hidden field
      var secondaryHidden = document.getElementById('secondaryContact');
      // assign secondaryContactTextbox to secondaryHidden value prop
      secondaryHidden.value = secondaryContactTextbox;
      // get refernce to html hidden field
      var hdnSecondary = document.getElementById('hdnSecondary');
      // hdnSecondary value prop contains asp.net label
      hdnSecondaryLabel.value = label2;
   }

   $(document).ready(function () {
      $('a').click(function (event) {
         var stateClickManager = document.getElementById('stateClickManager');
         stateClickManager.value = event.target.id;
      });

      Shadowbox.init({
         onOpen: setUp
      });
   });
</script>
 
Let's drop in the child page. Add new aspx page called FindContact. This will basically act as the search form.

<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="FindContact.aspx.cs" Inherits="WebForms.FindContact" >

<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
   <div id="container">
      <asp:Label runat="server" ID="lblFirstName">First Name</asp:Label>
      <asp:TextBox runat="server" ID="txtFirstName"></asp:TextBox>
      <br /><br />
      <asp:Label runat="server" ID="lblLastName">Last Name</asp:Label>
      <asp:TextBox runat="server" ID="txtLastName"></asp:TextBox>
      <asp:Button runat="server" ID="btnFind" Text="Find" OnClick="SearchForContact" ClientIDMode="Static" />
   </div>
   <br /><br />
   <div>
      <asp:ListBox runat="server" ID="searchResults" Width="400px" />
      <br /><br />
      <input type="button" id="btnClose" value="Select Contact" onclick="ClosePage()" />
   </div>
</asp:Content>
 
The code behind, FindContact.aspx.cs, is quite simple:

public partial class FindContact : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
   }

   protected void SearchForContact(object sender, EventArgs e)
   {
      // just hardcoding some data to use
      searchResults.Items.AddRange(new ListItem[]
      {
         new ListItem("Sheldon Cooper"),
         new ListItem("Leonard Hoffstadter")
      });
   }

   protected void btnSelectContact_Click(object sender, EventArgs e)
   {
      // simple redirect would work here
      Response.Redirect("Default.aspx?SelectedContact=" + searchResults.SelectedItem.Text);
   }
}
 
 
Again, to keep things simple, JavaScript will be in the page. The script here will be taking the selected values and sentting them to the text boxes in the parent.
 
<script type="text/javascript">
function ClosePage() {
   var shadowBox = window.parent.Shadowbox;
   var selectedItem = document.getElementById('<%=searchResults.ClientID %>').value;
   var stateManager = window.parent.document.getElementById('stateClickManager');
   var whoClicked = stateManager.value;

   var hiddenContactField;
   var textbox;
   var hiddenInput;
   var hiddenInputText;

   switch (whoClicked) {
      case "MainContent_hlnkPrimaryFind":
         // grab reference to primaryContact hidden field
         hiddenContactField = window.parent.document.getElementById('primaryContact');
         // hiddenContactField.value should be primaryContactTextBox
         textbox = window.parent.document.getElementById(hiddenContactField.value);
         // get reference to html hidden input
         hiddenInput = window.parent.document.getElementById('hdnPrimary');
         // get hiddenInput, assign value prop
         hiddenInputText = window.parent.document.getElementById(hiddenInput.value);
         break;

      case "MainContent_hlnkSecondaryFind":
         // grab reference to secondaryContact hidden field
         hiddenContactField = window.parent.document.getElementById('secondaryContact');
         // hiddenContactField.value should be secondaryContactTextBox
         textbox = window.parent.document.getElementById(hiddenContactField.value);
         // get reference to html hidden input
         hiddenInput = window.parent.document.getElementById('hdnSecondary');
         // get hiddenInput, assign value prop
         hiddenInputText = window.parent.document.getElementById(hiddenInput.value);
         break;
   }

   // will display the hidden field in parent
   $(hiddenInputText).text(selectedItem);
   // will assign selected item to textbox in parent
   textbox.value = selectedItem;
   shadowBox.close();
}
</script>
 
Things should be ready to go. Run, the screen....
Click Find next to Primary Contact, this will launch Shadowbox....
 

As a shortcut, click the Find button....

 
Select Sheldon Cooper, then click Select Contact....
 

 
Sheldon now appears in the Primary Contact textbox, also, the hidden field is displayed....
 
Select Find next to Secondary Contact, this will launch Shadowbox again....
Select Find again, select Leonard Hoffstadter, click Select Contact....
Leonard now appears in the Secondary Contact textbox, also, the hidden field is displayed....