Jeff Saracco

random thoughts, ideas, ramblings.

Ruby on Rails Polymorphic User Model With Devise Authentication

When modeling my application I have two types of users that have a polymorphic association to the user model. Such as:

My three classes with polymorphic relationships
1
2
3
4
5
6
7
8
9
10
11
class User < ActiveRecord::Base
    belongs_to :profileable, :polymorphic => true
end

class User_Type_1 < ActiveRecord::Base
    has_one :user, :as => :profileable
end

class User_Type_2 < ActiveRecord::Base
    has_one :user, :as => :profileable
end

The reason I did this, instead of an STI, is because User_Type_1 has something like 4 fields and User_Type_2 has something like 20 fields and I didn’t want the user table to have so many fields (yes 24-ish fields is not a lot but I’d rather not have ~20 fields empty most of the time)

The problem I was facing at this point was I want the sign up form to only be used to sign up users of type User_Type_1 but the sign in form to be used to both. (I will have an admin side of the application which will create users of User_Type_2)

I knew I can use the after_sign_in_path_for(resource) override in AppicationController somehow to redirect to the right part of the site on sign in. Something like:

Overriding the Devise after_sign_in_path_for method
1
2
3
4
5
6
7
8
def after_sign_in_path_for(resource)
    case current_user.profileable_type
    when "UserType1"
        return user_type_1_index_path
    when "UserType2"
        return user_type_1_index_path
    end
end

To achieve what I wanted here I just created a normal form for the User_Type_1 with nested attributes for User and had it post to the UserType1Controller:

User sign up form
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
= form_for :user_type_1 do |f|
  = f.label :first_name
  = f.text_field :first_name
  = f.label :last_name
  = f.text_field :last_name
  = f.label :phone_number
  = f.text_field :phone_number
  = fields_for :user do |user_fields|
    = user_fields.label :email
    = user_fields.email_field :email
    = user_fields.label :password
    = user_fields.password_field :password
    = user_fields.label :password_confirmation
    = user_fields.password_field :password_confirmation
  = f.submit

Then saved both objects and called the sign_in_and_redirect helper from Devise

saving the user and user_type_1 objects on create
1
2
3
4
5
6
7
8
9
10
11
12
class UserType1Controller < ApplicationController
    ...
    def create
        @user = User.new(params[:user])
        @user_type_1 = UserType1.new(params[:patron])
        @user.profileable = @user_type_1
        @user_type_1.save
        @user.save
        sign_in_and_redirect @user
    end
    ...
 end

Then the after_sign_in_path_for method from above sent it to the right place and it was all good.

ASP.NET C# - Catch the SelectedIndexChanged of a DropDownList Inside a Repeater

When there is a Dropdownlist inside a repeater the dropdownlist will not throw the SelectedIndexChanged event even when the AutoPostback property is set to true. To get this event to throw I had to bind the dropdownlist on the ItemDataBound event of the repeater and set the event handler for the SelectedIndexChanged event, as seen below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void rptrCandidates_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
     if ((e.Item.ItemType == ListItemType.Item) || (e.Item.ItemType == ListItemType.AlternatingItem))
     {
         DropDownList actions = e.Item.FindControl("actionsDDL") as DropDownList;
         if ((actions != null))
         {
               ListItemCollection _items = new ListItemCollection();
               foreach (string s in options)
               {
                    _items.Add(new ListItem(s, row.ProjectCandidateId.ToString()));
               }
               actions.SelectedIndexChanged += new EventHandler(actionsDDL_SelectedIndexChanged);
               actions.DataSource = _items;
               actions.DataBind();
          }
     }
}

However, when the dropdownlist is bound the DataTextField and DataValueField are both filled with the Text value, and when I try to explicity set these, the SelectedIndexChanged event is not thrown. To get around this bizarre feature what I had to do was, next to the dropdownlist, put a hiddenfield that got bound to it the correct value that was supposed to be in the Value field of the list items (all were to have the same value):

1
2
3
4
5
<ItemTemplate>
    <asp:HiddenField ID="projectCandidateHF" runat="server" Value='<%# Eval("ProjectCandidateId") %>' />
    <asp:DropDownList ID="actionsDDL" runat="server" AutoPostBack="true" SelectedIndexChanged="actionsDDL_SelectedIndexChanged">
    </asp:DropDownList>
</ItemTemplate>

Then on the SelectedIndexChanged event that the dropdownlist throws use the sender to get the DropDownList that threw the event and then use the Parent property to get the RepeaterItem in which it lives, then find the HiddenField using FindControl method:

1
2
3
4
5
6
protected void actionsDDL_SelectedIndexChanged(object sender, EventArgs e)
{
    DropDownList actionsDDL = sender as DropDownList;
    HiddenField projectCandidateIdHF = actionsDDL.Parent.FindControl("projectCandidateHF") as HiddenField;
    //do fun stuff here
}

ASP MVC OnActionExecuting Redirect Without Executing Action

I had a bug where an exception was being thrown for a null object in an action, so I decided to make an ActionFilter to check that the user had the right role and if not, redirect to default.aspx (which, it turns out, redirects to the correct dashboard, super bonus!). It worked, however the action was still being executed and throwing the error before the redirect happened.

The fix for this, to stop execution of the action is set the result of the filterContext object like so:

1
2
3
4
5
6
7
8
9
10
public class RequiresFirmRoleAttribute : ActionFilterAttribute
{
     public override void OnActionExecuting(ActionExecutingContext filterContext)
     {
        if (!((BaseFirmController)filterContext.Controller).IsFirmAdmin)
        {
            filterContext.Result = new RedirectResult("/Default.aspx");
        }
     }
}

Dropping Partial MVC Views on Old School WebForms Pages

Original Source here with some changes due to difference in versions referenced.

I was refactoring some pages and needed to drop an MVC partial view on one of our WebForms (aspx) pages. However, this functionality does not exist natively so I had to get creative. This solution, in my opinion, was best for now because it was easier than creating two of the same control, one for MVC and one for WebForms. It was also easier than refactoring all of the WebForms pages to be MVC just so that they could use this one control (however, they will probably be upgraded at some point in the future).

Without further ado, here is the solution (if you didn.t already click the link at the top):

Create a blank WebFormController class and a WebFormMVCUtil class which will grab the view, bind the model to it and write it to the screen. The code (located in public/Controllers/WebFormController.cs) is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class WebFormController : Controller { }

    public static class WebFormMVCUtil
    {

        public static void RenderPartial(string partialName, string controller, object model)
        {
            //get a wrapper for the legacy WebForm context
            var httpCtx = new HttpContextWrapper(HttpContext.Current);

            //create a mock route that points to the empty controller
            var rt = new RouteData();
            rt.Values.Add("controller", controller);

             //create a controller context for the route and http context
            var ctx = new ControllerContext(new RequestContext(httpCtx, rt), new WebFormController());

             //find the partial view using the viewengine
            var view = ViewEngines.Engines.FindPartialView(ctx, partialName).View;

             //create a view context and assign the model
            var vctx = new ViewContext(ctx, view,
                new ViewDataDictionary { Model = model },
                new TempDataDictionary(), HttpContext.Current.Response.Output);

            //render the partial view
            view.Render(vctx, HttpContext.Current.Response.Output);
        }
    }
}

If you don.t care how it is implemented and just want to know how to stick an MVC partial view on your WebForms page just add the line below (substituting your own controller and view names in)

1
<% WebFormMVCUtil.RenderPartial("ViewName", "ControllerName", model); %>

Also, depending on your view, you need to pass it the model; but, as I did here, you can place a method in there that will get the model for you.

ASP MVC - Fix for IE Caching Everything

So it turns out that IE aggressively caches every AJAX GET response, which makes for some interesting and hard to fix bugs since we moved a lot of functionality to AJAX calls with MVC.

Fear not, I have found a solution for this. The solution is to put a custom attribute on your MVC GET action if it is being called through AJAX. For Instance, the GET method for getting a list may look like this:

1
2
[CacheControl(HttpCacheability.NoCache), HttpGet]
public ActionResult MyAction(int param1, int param2)

CacheControl(HttpCacheability.NoCache) is the attribute you should put on your MVC action in order for IE to not cache anything.

If all you want to know is how to fix this bug, you can stop reading here, if you want to know what it is doing, keep going.

The way this works is creating a class that inherits from the ActionFilterAttribute class and overriding the OnActionExecuted method and adding some no-cache information to the headers being sent out. Full class below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CacheControlAttribute : ActionFilterAttribute
{
        public CacheControlAttribute(HttpCacheability cacheability)
        {
            _cacheability = cacheability;
        }

        private readonly HttpCacheability _cacheability;

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
            cache.SetCacheability(_cacheability);
            cache.SetExpires(DateTime.Now);
            cache.SetAllowResponseInBrowserHistory(false);
            cache.SetNoServerCaching();
            cache.SetNoStore();

  }
}