Entity Framework 4 w/ MVC 2 - fighting against EF4's inability to allow empty strings in form fields
FINAL EDIT: Success! There's a data annotation rule that stops empty strings from being set to null. I was able to use the solution here (Server-side validation of a REQUIRED String Property in MVC2 Entity Framework 4 does not work). Works like a charm now. I'll keep the rest of my post as-is in case anyone can learn from it.
Decided to rewrite this post to give all the detail I can. Long post, given all the code, so please bear with me.
I'm writing an MVC 2 project using EF4 as a base. All of my db columns are non-nullable.
Like I said before, I'm having issues with EF4 throwing a ConstraintException when I test the case of an empty form. The exception is springing up from the Designer.cs file, specifically in code like:
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.String GameTitle
{
get
{
return _GameTitle;
}
set
{
OnGameTitleChanging(value);
ReportPropertyChanging("GameTitle");
_GameTitle = StructuralObject.SetValidValue(value, false); // <-- this is where the exception is being thrown
ReportPropertyChanged("GameTitle");
OnGameTitleChanged();
}
}
private global::System.String _GameTitle;
partial void OnGameTitleChanging(global::System.String value);
partial void OnGameTitleChanged();
The exception IS NOT being caught and handled by MVC. Instead, I keep getting an uncaught exception and a YSoD. My controller (excuse the mess - I was trying to get repository level validation to work, see below):
[HttpPost]
public ActionResult CreateReview([Bind(Prefix = "GameData")]Game newGame, int[] PlatformIDs)
{
/* try
{
_gameRepository.ValidateGame(newGame, PlatformIDs);
}
catch (RulesException ex)
{
ex.CopyTo(ModelState);
}
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
else
{
var genres = _siteDB.Genres.OrderBy(g => g.Name).ToList();
var platforms = _siteDB.Platforms.OrderBy(p => p.PlatformID).ToList();
List<PlatformListing> platformListings = new List<PlatformListing>();
foreach(Platform platform in platforms)
{
platformListings.Add(new PlatformListing { Platform = platform, IsSelected = false });
}
var model = new AdminGameViewModel { GameData = newGame, AllGenres = genres, AllPlatforms = platformListings };
return View(model);
}*/
if (PlatformIDs == null || PlatformIDs.Length == 0)
{
ModelState.AddModelError("PlatformIDs", "You need to select at least one platform for the game");
}
try
{
foreach(var p in PlatformIDs)
{
Platform plat = _siteDB.Platforms.Single(pl => p == pl.PlatformID);
newGame.Platforms.Add(plat);
}
newGame.LastModified = DateTime.Now;
if (ModelState.IsValid)
{
_siteDB.Games.AddObject(newGame);
_siteDB.SaveChanges();
return RedirectToAction("Index");
}
else
{
return View();
}
}
catch
{
return View();
}
}
I've tried several things to get validation to work. The first was Steven Sanderson's "Let the model handle it" as described in his aPress MVC2 book. I figured the best place to attempt validation would be in the repository, since the data would have to be passed in there anyway, and I'd like to keep my controller thin. The repo:
public class HGGameRepository : IGameRepository
{
private HGEntities _siteDB = new HGEntities();
public List<Game> Games
{
get { return _siteDB.Games.ToList(); }
}
public void ValidateGame(Game game, int[] PlatformIDs)
{
var errors = new RulesException<Game>();
if (string.IsNullOrEmpty(game.GameTitle))
{
errors.ErrorFor(x => x.GameTitle, "A game must have a title");
}
if (string.IsNullOrEmpty(game.ReviewText))
{
errors.ErrorFor(x => x.ReviewText, "A review must be written");
}
if (game.ReviewScore <= 0 || game.ReviewScore > 5)
{
errors.ErrorFor(x => x.ReviewScore, "A game must have a review score, and the score must be between 1 and 5");
}
if (string.IsNullOrEmpty(game.Pros))
{
errors.ErrorFor(x => x.Pros, "Each game review must have a list of pros");
}
if (string.IsNullOrEmpty(game.Cons))
{
errors.ErrorFor(x => x.Cons, "Each game review must have a list of cons");
}
if (PlatformIDs == null || PlatformIDs.Length == 0)
{
errors.ErrorForModel("A game must belong to at least one platform");
}
if (game.Genre.Equals(null) || game.GenreID == 0)
{
errors.ErrorFor(x => x.Genre, "A game must be associated with a genre");
}
if (errors.Errors.Any())
{
throw errors;
}
else
{
game.Platforms.Clear(); // see if there's a more elegant way to remove changed platforms
foreach (int id in PlatformIDs)
{
Platform plat = _siteDB.Platforms.Single(pl => pl.PlatformID == id);
game.Platforms.Add(plat);
}
SaveGame(game);
}
}
public void SaveGame(Game game)
{
if (game.GameID == 0)
{
_siteDB.Games.AddObject(game);
}
game.LastModified = DateTime.Now;
_siteDB.SaveChanges();
}
public Game GetGame(int id)
{
return _siteDB.Games.Include("Genre").Include("Platforms").SingleOrDefault(g => g.GameID == id);
}
public IEnumerable<Game> GetGame(string title)
{
return _siteDB.Games.Include("Genre").Include("Platforms").Where(g => g.GameTitle.StartsWith(title)).AsEnumerable<Game>();
}
public List<Game> GetGamesByGenre(int id)
{
return _siteDB.Games.Where(g => g.GenreID == id).ToList();
}
public List<Game> GetGamesByGenre(string genre)
{
return _siteDB.Games.Where(g => g.Genre.Name == genre).ToList();
}
public List<Game> GetGamesByPlatform(int id)
{
return _siteDB.Games.Where(g => g.Platforms.Any(p => p.PlatformID == id)).ToList();
}
public List<Game> GetGamesByPlatform(string platform)
{
return _siteDB.Games.Where(g => g.Platforms.Any(p => p.Name == platform)).ToList();
}
}
}
The RulesException/Rule Violation classes (as taken from his book):
public class RuleViolation
{
public LambdaExpression Property { get; set; }
public string Message { get; set; }
}
public class RulesException : Exception
{
public readonly IList<RuleViolation> Errors = new List<RuleViolati开发者_开发问答on>();
private readonly static Expression<Func<object, object>> thisObject = x => x;
public void ErrorForModel(string message)
{
Errors.Add(new RuleViolation { Property = thisObject, Message = message });
}
}
public class RulesException<TModel> : RulesException
{
public void ErrorFor<TProperty>(Expression<Func<TModel, TProperty>> property, string message)
{
Errors.Add(new RuleViolation { Property = property, Message = message });
}
}
That did not work, so I decided to try Chris Sells' method of using IDataErrorInfo (as seen here: http://sellsbrothers.com/Posts/Details/12700). My code:
public partial class Game : IDataErrorInfo
{
public string Error
{
get
{
if (Platforms.Count == 0)
{
return "A game must be associated with at least one platform";
}
return null;
}
}
public string this[string columnName]
{
get
{
switch (columnName)
{
case "GameTitle":
if (string.IsNullOrEmpty(GameTitle))
{
return "Game must have a title";
}
break;
case "ReviewText":
if (string.IsNullOrEmpty(ReviewText))
{
return "Game must have an associated review";
}
break;
case "Pros":
if (string.IsNullOrEmpty(Pros))
{
return "Game must have a list of pros";
}
break;
case "Cons":
if (string.IsNullOrEmpty(Cons))
{
return "Game must have a list of cons";
}
break;
}
return null;
}
}
}
}
Again, it did not work.
Trying simple data annotations:
[MetadataType(typeof(GameValidation))]
public partial class Game
{
class GameValidation
{
[Required(ErrorMessage="A game must have a title")]
public string GameTitle { get; set; }
[Required(ErrorMessage = "A game must have an associated review")]
public string ReviewText { get; set; }
[Required(ErrorMessage="A game must have a list of pros associated with it")]
public string Pros { get; set; }
[Required(ErrorMessage="A game must have a set of cons associated with it")]
public string Cons { get; set; }
}
}
Also did not work.
The common denominator with all of this is the EF designer throwing an exception, and the exception not being caught by MVC. I'm at a complete loss.
EDIT: Cross-posted from here: http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/643e0267-fc7c-44c4-99da-ced643a736bf
Same issue with someone else: http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/4fee653a-4c8c-4a40-b3cf-4944923a8c8d/
Personally i don't see a point in storing this in the database:
Id Description
1 Foo
2 ''
3 Bar
Seems silly to me.
Instead of storing blank fields you make it nullable.
If you disagree, then you could set a default value in the database, and set the StoreGeneratedPattern to Computed.
Or you could have a ctor for the entity, which sets the value.
精彩评论