It seems strange to me that MVC doesn't handle posting a file and a model at the same time from AJAX by default. It'd be great if you could just send your data and "poof" it's all there for you. It doesn't seem to be, so I'll put down what I've found that works for me.
You create a javascript FormData object. Add your model (with property "model") and your file('s). Post as normal.
In your action method, remove any model parameter you had. You won't need it. Just look at the Request.Form["model"] and deserialize your json. Get your files in the Request.Form collection.
Tuesday, June 17, 2014
Tuesday, September 10, 2013
MVC 4 Project File Structure
I've been trying to think of a clean way to structure web app folders that keeps things simple and removes clutter.Typically in most web apps, all scripts are thrown willy-nilly into the "Scripts" folder. You end up with tons of js files with no real structure to them. And, think about it... How often do you modify the jQuery source? How about Bootstrap? Modernizr?
Right... Why have them in the most visible place in the folder? The files you modify regularly should be easy to get to. The files that are never modified should be tucked away, only changed when upgrading or removing. We want the most often modified files visible and easily navigated. I came up with the following.
Which works well for a typical non-MS website... However, with MVC, it's not standard practice to have the js for a view/partial in the folder where it resides. Also, you can install and upgrade client libraries using NuGet. Unfortunately you don't get a choice as to where these files are installed. So, you're stuck with a "Content" and "Scripts" folder, and unless you modify your routing, you can't put script files in view folders.
To try and keep upgrade functionality but remove clutter, I came up with this compromise...
The key features here are the Content/App folder, the Scripts/App folder, and the ViewScripts folder. By having special subfolders in Content and Scripts, I get away from having files that I modify from being lost in the files I install through NuGet. Typically these files are modified infrequently anyway, but at least this way they're easy to find. How many times have you found derelict css files lost in the Content folder mess? This helps with that.
The biggest benefit comes from moving view specific files to the ViewScripts folder. This folder follows the directory structure of our Views folder. For views/partials that need a specific javascript folder, you merely create one in the ViewScripts folder. Easy to find, easy to navigate. I'm sure there are a million ways to lay out the directory structure of an MVC app. This one works for me. I'm mainly posting this as a personal reference :)
Right... Why have them in the most visible place in the folder? The files you modify regularly should be easy to get to. The files that are never modified should be tucked away, only changed when upgrading or removing. We want the most often modified files visible and easily navigated. I came up with the following.
Generic Folder Structure
- common
- js
- jQuery
- knockout
- bootstrap
- css
- jQuery
- knockout
- bootstrap
- images
- Any file structure that makes sense. Most image resources for js libraries are contained in their respective folders inside of the css folder
- fx (or framework)
- app.js (global settings specific to this application)
- fx.js (common functions that are a part of the company framework code)
- Any other files that are specific to the company and are used across the site.
- views
- account
- login.html
- login.js
- hearing
- hearingSummary.html
- hearingSummary.js
- hearingResult.html
- hearingResult.js
- hearings.html
- hearings.js
- common
- any control templates that are used elsewhere in the application.
- index.html
- index.js
Which works well for a typical non-MS website... However, with MVC, it's not standard practice to have the js for a view/partial in the folder where it resides. Also, you can install and upgrade client libraries using NuGet. Unfortunately you don't get a choice as to where these files are installed. So, you're stuck with a "Content" and "Scripts" folder, and unless you modify your routing, you can't put script files in view folders.
To try and keep upgrade functionality but remove clutter, I came up with this compromise...
- [] Content
- [] App
- [] img
- any images specific to the application
- site.css
- [] kendo
- [] font
- [] Controllers
- [] Models
- [] Scripts
- [] App
- fx.js
- util.js
- etc...
- [] kendo
- bootstrap.js
- jquery.js
- knockout.js
- etc...
- [] Views
- [] ViewScripts
- [] Blog
- index.js
- editBlog.js
- newBlog.js
- [] Shared
- layout.js
- somePartial.js
- anotherPartial.js
- etc...
- [] Security
- login.js
- users.js
- etc...
The key features here are the Content/App folder, the Scripts/App folder, and the ViewScripts folder. By having special subfolders in Content and Scripts, I get away from having files that I modify from being lost in the files I install through NuGet. Typically these files are modified infrequently anyway, but at least this way they're easy to find. How many times have you found derelict css files lost in the Content folder mess? This helps with that.
The biggest benefit comes from moving view specific files to the ViewScripts folder. This folder follows the directory structure of our Views folder. For views/partials that need a specific javascript folder, you merely create one in the ViewScripts folder. Easy to find, easy to navigate. I'm sure there are a million ways to lay out the directory structure of an MVC app. This one works for me. I'm mainly posting this as a personal reference :)
Wednesday, August 21, 2013
Javascript Phone Number Knockout Binding
Hey, all...
Over the years, I must have written a dozen phone number textboxes. Unfortunately I never save the code. I get to play with Knockoutjs at my new job so I figured I'd give the number textbox another shot.
Over the years, I must have written a dozen phone number textboxes. Unfortunately I never save the code. I get to play with Knockoutjs at my new job so I figured I'd give the number textbox another shot.
ko.bindingHandlers.phoneNumber = { formatNumber: function (box) { var box = $(box); var value = box.data("value"); if (value.length <= 10) { value = value.replace(/(\d\d\d)(\d\d\d)(\d\d\d\d)/, '($1) $2-$3'); } else { value = box.data("value").replace(/(\d\d\d)(\d\d\d)(\d\d\d\d)(\d+)/, '($1) $2-$3 x$4'); } $(box).val(value); }, init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var _this = ko.bindingHandlers.phoneNumber; var textbox = $(element); //function formatNumber(box) { //} textbox.keydown(function (e) { // allow cut/copy/paste if (e.ctrlKey && (e.which === 67 || e.which === 86|| e.which === 88)) { return; } // allow delete and backspace if (e.which === 8 || e.which === 46) { return; } // Ensure that a number was pushed if (e.shiftKey || !((e.which >= 48 && e.which <= 57) || (e.which >= 96 && e.which <= 105))) { e.preventDefault(); } }); textbox.focusin(function (e) { var value = textbox.data("value") || ""; textbox.val(value); }); textbox.focusout(function (e) { var value = textbox.val().replace(/[^0-9]+/g, ''); textbox.data("value", value); _this.formatNumber(this); valueAccessor()(value); }); }, update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var _this = ko.bindingHandlers.phoneNumber; var textbox = $(element); var value = valueAccessor()().replace(/[^0-9]+/g, ''); textbox.data("value", value); _this.formatNumber(textbox); } }
Tuesday, March 19, 2013
Paychex and Horrible Web Applications
Our company recently chose Paychex as our HR portal, benefits management and payroll company. I've gotten paystubs from Paychex before, and thought our move might be a good one. Was I ever wrong.
To view my HR tools, like W2 info, paystubs, Direct Deposit I go to one site. This site takes a username, password and a company ID.
It uses ASP.Net, probably 2.0. When I view the page in Chrome, everything is disabled by default. I have to refresh the page in order to interact with it. The entire site is riddled with slow postbacks, often when choosing a dropdown (instead of a less obtrusive update panel). Despite its looks and poor performance, most of the site actually works, just keep clicking. You might have to click places that make no sense. Some of this is easier in IE, but not much.
To view and enter time and request time off, you go to another site. This site takes a username, a password, and a different company ID. I'm sure this is done for security. (wat).
This site has a completely different look and feel, yet done in that ever-so-quaint ASP.Net 2.0 fashion, again using whole page postbacks to give it that "web 1.1" feel. Navigation here is atrocious. You can "clock in" on the first page, but as salary, I want to manage my time all in one shot. (Don't get me started on why we're having to report our time for each day). There's a different section where you can do this. It uses what looks like a pageable repeater. No styling on it. Just dragged and dropped into the editor, there ya go.
Well, you can select a time type from a dropdown. Of course, changing this dropdown causes a full page postback. Once you enter time and submit, you can't change it. And, of course there is no validation (server or client) so entering 80 hours for one day is perfectly fine.
There's ANOTHER section you have to go to in order to edit time. And you don't "edit" it, you delete it for that day and re-enter.
Well, Paychex has created a "revolutionary" "new" way to manage this! MyPaychex.com! This has a more streamlined look. If you look past the javascript error dialogs that show in IE as soon as you log in.
Trying to register is a complete pain, as changing options forces a postback that of course clears any text you entered before the postback happened.
You have to bind the other accounts you have with this one, as all this site is, is a tab container that holds iframes to the other websites.
Yeah... So Paychex... IMO you need to redo your web infrastructure. And be willing to pay for new technology done right.
To view my HR tools, like W2 info, paystubs, Direct Deposit I go to one site. This site takes a username, password and a company ID.
It uses ASP.Net, probably 2.0. When I view the page in Chrome, everything is disabled by default. I have to refresh the page in order to interact with it. The entire site is riddled with slow postbacks, often when choosing a dropdown (instead of a less obtrusive update panel). Despite its looks and poor performance, most of the site actually works, just keep clicking. You might have to click places that make no sense. Some of this is easier in IE, but not much.
To view and enter time and request time off, you go to another site. This site takes a username, a password, and a different company ID. I'm sure this is done for security. (wat).
This site has a completely different look and feel, yet done in that ever-so-quaint ASP.Net 2.0 fashion, again using whole page postbacks to give it that "web 1.1" feel. Navigation here is atrocious. You can "clock in" on the first page, but as salary, I want to manage my time all in one shot. (Don't get me started on why we're having to report our time for each day). There's a different section where you can do this. It uses what looks like a pageable repeater. No styling on it. Just dragged and dropped into the editor, there ya go.
Well, you can select a time type from a dropdown. Of course, changing this dropdown causes a full page postback. Once you enter time and submit, you can't change it. And, of course there is no validation (server or client) so entering 80 hours for one day is perfectly fine.
There's ANOTHER section you have to go to in order to edit time. And you don't "edit" it, you delete it for that day and re-enter.
Well, Paychex has created a "revolutionary" "new" way to manage this! MyPaychex.com! This has a more streamlined look. If you look past the javascript error dialogs that show in IE as soon as you log in.
Trying to register is a complete pain, as changing options forces a postback that of course clears any text you entered before the postback happened.
You have to bind the other accounts you have with this one, as all this site is, is a tab container that holds iframes to the other websites.
Yeah... So Paychex... IMO you need to redo your web infrastructure. And be willing to pay for new technology done right.
Tuesday, February 12, 2013
CSS and Filling the Full Height of your Page
I'd posted on Stack Overflow on this topic a while ago and wanted to address it again. My initial post wasn't very good, so I've used JSBin to correct this.
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>JS Bin</title>
</head>
<body>
<div class="heading">
<h1>My Heading</h1>
</div>
<div class="content"><div class="height-third">
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
</div>
<div class="height-third">
Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
</div>
</div>
</body>
</html>
http://jsbin.com/onubed/1/
In case that link dies...
HTML
<html>
<head>
<meta charset=utf-8 />
<title>JS Bin</title>
</head>
<body>
<div class="heading">
<h1>My Heading</h1>
</div>
<div class="content"><div class="height-third">
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
</div>
<div class="height-third">
Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
</div>
</div>
</body>
</html>
CSS
*{
box-sizing: border-box;
}
.heading{
background-color: #FDD;
position: relative;
}
.content{
top: 100px;
background-color: #DFD;
bottom: 0px;
position: absolute;
min-height: 200px;
}
.height-third{
position: relative;
height: 33%;
overflow: hidden;
background-color: #DDF;
}
Tuesday, January 15, 2013
Suggestion for App Project Separation
Stolen from a chat with my friend +Chris Faulhaber ... A great breakdown of projects in a solution for an application.
- Project.Domain:
- The structure, business rules, and persistence logic. Every effort should be made to place code here instead of in UI projects. No UI code should ever go here.
- Project.Web:
- The web UI and controller code for the project. Self-explanatory.
- Project.Database:
- Any DB scripts.
- Project.Service:
- If the project has a windows service, it usually goes here. This is typically for polling services that have short or unpredictable intervals. Long and predictable intervals should be setup as console apps that are run through the Windows scheduler.
- Project.Tests:
- Unit tests.
- Organization.Platform or Organization.Core:
- Shared code for multiple projects within an organization. This can be further split into Organization.Platform.Domain and Organization.Platform.Web if needed (following the scheme above), but that's uncommon.
Thursday, October 11, 2012
Watch Your Views...
Today I was driven crazy by what I thought was an nHibernate bug. I'd changed the mappings for maybe 6 entities in this project, trying to get a crazy join to work in QueryOver. Although that still doesn't work (nHibernate generated sql isn't including the last table in the join, yet adds the alias for the missing table to the where clause), I ran into another issue while testing.
In part of our app, you can drill down 3 levels, to the detail of one of our aggregate roots. Well, the last level wasn't working. I don't believe I modified any mappings that should affect this object, so I was stumped. The nHProf output was odd. It said
Failed to execute multi criteria[SQL: SELECT count(*) as y0_ FROM ...
WHERE ((this_.MyObjectId= ? and this_.OtherObjectId = ?) and this_.AnotherObjectId = ?);
SELECT TOP (?) this_.FieldName, ...
So I ask myself... Where did these question marks come from? I can't find a thing online that explains it.
In the error message thrown by Visual Studio I get...
And then this cryptic message block
Um... wha?!? So, somewhere NHibernate is trying to convert something to a double and is failing. Okay, why is it doing that and why is that causing NHibernate to add question marks to the query?
I check out the mappings. Notice that some of the double fields on this aggregate are not nullable when the fields in the view they are populated by is set up to be nullable. Strange. I remember that normally it throws an error that kind of points you to this fact, but I fix it and proceed.
Still no dice.
I check out the query its generating, I check the mapping again, I check everything I can think of... Hours later, I'm going over it again and I start looking at the data. I check the result set for nulls where it should be a non null. Nothing. Then I start comparing the data from the view to the column types.
I find something interesting. We have a double field called Margin. The value returned for this field from SELECT * FROM MyFeature.MyView is '2012-07-01'. My friends, that is NOT a double! I'd missed it my first couple passes because I'd always believed that the types given to a column are inviolate. SQL Server wouldn't lie to me!
Well... Someone updated one of the tables that this view relies on. Apparently they changed the order of some columns. And since the underlying view structure is based (from what I understand) on column index, SQL Server said, "Hey! This is now a Date field!", and started returning values for a different column.
WTH!?!
I'd never even HEARD of such a thing! Called over the DBA and asked him what he thought about a date appearing in a double column and he explained to me what happened. Even gave me the proc he uses to regen all views in a given database. Regenerated the view and voila! Bug is gone :)
If I were a better person, I'd dig into NHibernate's source and offer a patch that gives a better error message for this issue.
Instead I'm writing this blog post, that outlines my frustration, but has all of the keywords I searched for in my initial journey to find the solution to this problem. Hopefully another developer having this issue will stumble upon my blog and waste less time than I did.
Moral of the story...
Don't trust SQL to always return the type you expect
And
If you can't find the solution to your issue on Google, maybe you really have a different problem.
In part of our app, you can drill down 3 levels, to the detail of one of our aggregate roots. Well, the last level wasn't working. I don't believe I modified any mappings that should affect this object, so I was stumped. The nHProf output was odd. It said
Failed to execute multi criteria[SQL: SELECT count(*) as y0_ FROM ...
WHERE ((this_.MyObjectId= ? and this_.OtherObjectId = ?) and this_.AnotherObjectId = ?);
SELECT TOP (?) this_.FieldName, ...
So I ask myself... Where did these question marks come from? I can't find a thing online that explains it.
In the error message thrown by Visual Studio I get...
{"Input string was not in a
correct format."}
And then this cryptic message block
at
System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo
numfmt)
at
System.String.System.IConvertible.ToDouble(IFormatProvider provider)
at
System.Convert.ToDouble(Object value)
at
NHibernate.Type.DoubleType.Get(IDataReader rs, Int32 index)
at
NHibernate.Type.NullableType.NullSafeGet(IDataReader rs, String name)
at
NHibernate.Type.NullableType.NullSafeGet(IDataReader rs, String[] names,
ISessionImplementor session, Object owner)
at
NHibernate.Type.AbstractType.Hydrate(IDataReader rs, String[] names,
ISessionImplementor session, Object owner)
at
NHibernate.Persister.Entity.AbstractEntityPersister.Hydrate(IDataReader rs,
Object id, Object obj, ILoadable rootLoadable, String[][]
suffixedPropertyColumns, Boolean allProperties, ISessionImplementor session)
at
NHibernate.Loader.Loader.LoadFromResultSet(IDataReader rs, Int32 i, Object obj,
String instanceClass, EntityKey key, String rowIdAlias, LockMode lockMode,
ILoadable rootPersister, ISessionImplementor session)
at
NHibernate.Loader.Loader.InstanceNotYetLoaded(IDataReader dr, Int32 i,
ILoadable persister, EntityKey key, LockMode lockMode, String rowIdAlias,
EntityKey optionalObjectKey, Object optionalObject, IList hydratedObjects,
ISessionImplementor session)
at
NHibernate.Loader.Loader.GetRow(IDataReader rs, ILoadable[] persisters,
EntityKey[] keys, Object optionalObject, EntityKey optionalObjectKey,
LockMode[] lockModes, IList hydratedObjects, ISessionImplementor session)
at
NHibernate.Loader.Loader.GetRowFromResultSet(IDataReader resultSet,
ISessionImplementor session, QueryParameters queryParameters, LockMode[]
lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[]
keys, Boolean returnProxies)
at
NHibernate.Impl.MultiCriteriaImpl.GetResultsFromDatabase(IList results)
Um... wha?!? So, somewhere NHibernate is trying to convert something to a double and is failing. Okay, why is it doing that and why is that causing NHibernate to add question marks to the query?
I check out the mappings. Notice that some of the double fields on this aggregate are not nullable when the fields in the view they are populated by is set up to be nullable. Strange. I remember that normally it throws an error that kind of points you to this fact, but I fix it and proceed.
Still no dice.
I check out the query its generating, I check the mapping again, I check everything I can think of... Hours later, I'm going over it again and I start looking at the data. I check the result set for nulls where it should be a non null. Nothing. Then I start comparing the data from the view to the column types.
I find something interesting. We have a double field called Margin. The value returned for this field from SELECT * FROM MyFeature.MyView is '2012-07-01'. My friends, that is NOT a double! I'd missed it my first couple passes because I'd always believed that the types given to a column are inviolate. SQL Server wouldn't lie to me!
Well... Someone updated one of the tables that this view relies on. Apparently they changed the order of some columns. And since the underlying view structure is based (from what I understand) on column index, SQL Server said, "Hey! This is now a Date field!", and started returning values for a different column.
WTH!?!
I'd never even HEARD of such a thing! Called over the DBA and asked him what he thought about a date appearing in a double column and he explained to me what happened. Even gave me the proc he uses to regen all views in a given database. Regenerated the view and voila! Bug is gone :)
If I were a better person, I'd dig into NHibernate's source and offer a patch that gives a better error message for this issue.
Instead I'm writing this blog post, that outlines my frustration, but has all of the keywords I searched for in my initial journey to find the solution to this problem. Hopefully another developer having this issue will stumble upon my blog and waste less time than I did.
Moral of the story...
Don't trust SQL to always return the type you expect
And
If you can't find the solution to your issue on Google, maybe you really have a different problem.
Subscribe to:
Posts (Atom)