Domain Multiple inheritance and manyToMany relation removeFrom not working - grails

I prepared some complex model structure due to content management businnes.
My main content model likes as following code.
class CmsContent implements Comparable<CmsContent>, Taggable, Serializable {
Set<CmsContent> contents
static hasMany = [contents:CmsContent]
}
Other Content Model extends from above
class Menu extends CmsContent {
}
class Image extends CmsContent {
}
class Video extends CmsContent {
}
On Controller side when ever add Image to Menu it is perfectly working
def addContent(){
Menu menuInstance = Menu.get(params.id)
if (menuInstance == null) {
notFound()
return
}
CmsContent content = CmsContent.get(params.contentId)
if (content == null) {
notFound()
return
}
menuInstance.addToContents(content)
menuInstance.save(flush: true)
request.withFormat {
'*'{
def result =[:] ;
result.status ='success';
render result as JSON
}
}
}
def removeContent(){
Menu menuInstance = Menu.get(params.id)
if (menuInstance == null) {
notFound()
return
}
long id = Long.valueOf(params.contentId)
CmsContent content = cmsContent.contents.find { it.id == id }
if (content == null) {
notFound()
return
}
menuInstance.removeFromContents(content)
menuInstance.save(flush: true)
request.withFormat {
'*'{
def result =[:] ;
result.status ='success';
render result as JSON
}
}
}
But when I tried to remove facing following exception.
Cannot get property 'name' on null object. Stacktrace follows:
Message: Cannot get property 'name' on null object
I tracked down in grails code in DomainClassGrailsPlugin.
I realize that addTo Method and line 368 checking prop.otherSide attribute
if (prop.bidirectional && prop.otherSide) {
}
unfortunately removeTo method not check prop.otherSide attribute, therefore name field throwing the exception.
if (prop.bidirectional) {
if (prop.manyToMany) {
String name = prop.otherSide.name
arg[name]?.remove(delegate)
} else {
arg[prop.otherSide.name] = null
}
}
What do you think ? Do you agree with me ?

Related

Hide a header from being displayed in Swagger Swashbuckle

As I already have a common authorization added how do I remove separate(authorization header) from each and every API as shown in the image link below?
swaggerHub_Link
I figured it out:
You can simply create a custom attibute and an operation filter inhering from Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter in order to hide the header from being displayed in swagger.json
public class OpenApiHeaderIgnoreAttribute : System.Attribute
{
}
class name should end with the name of the base class.
public class OpenApiHeaderIgnoreFilter : Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter
{
public void Apply(Microsoft.OpenApi.Models.OpenApiOperation operation, Swashbuckle.AspNetCore.SwaggerGen.OperationFilterContext context)
{
if (operation == null || context == null || context.ApiDescription?.ParameterDescriptions == null)
return;
var parametersToHide = context.ApiDescription.ParameterDescriptions
.Where(parameterDescription => ParameterHasIgnoreAttribute(parameterDescription))
.ToList();
if (parametersToHide.Count == 0)
return;
foreach (var parameterToHide in parametersToHide)
{
var parameter = operation.Parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, parameterToHide.Name, System.StringComparison.Ordinal));
if (parameter != null)
operation.Parameters.Remove(parameter);
}
}
private static bool ParameterHasIgnoreAttribute(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription parameterDescription)
{
if (parameterDescription.ModelMetadata is Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata metadata)
{
return metadata.Attributes.ParameterAttributes.Any(attribute => attribute.GetType() == typeof(OpenApiParameterIgnoreAttribute));
}
return false;
}
}
and set this filter class in your startup class configureServices method as below:
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.OperationFilter<OpenApiHeaderIgnoreFilter>();
}
}
and also update your API method in controller as below:
[EnableQuery]
[SwaggerOperation(Tags = new[] { "Odata" })]
public async Task<IActionResult> Get([OpenApiHeaderIgnore] [FromHeader(Name = ClaimNames.AccessToken)] string token)
{
// code
}

How to do server-side validation using DataAnnotations

I am using ASP.NET MVC 5 and trying to do custom validation using data annotations of a view model object that is instantiated within the controller post method and reloaded from EF. Only a few of the model properties are populated when posting from the client and the rest are reloaded using EF. The model is complex and all parts of it implement IValidatableObject. The code within the Validate method fires for the new view model after everything is loaded correctly, but it does not do anything with the data annotations applied to the model.
So for instance, if my model has the EmailAddress attribute applied to a field, it works great on the client side, but it is ignored on the server side when validating against the new view model. How do I get my Validate method to take the data annotations into account?
Here's the code for the controller action...
[HttpPost]
[ValidateAntiForgeryToken]
public virtual ActionResult SignUp(SignUpEM em) {
if (em == null) {
return RedirectToAction(MVC.Sheet.Index());
}
SignUpSheet signUpSheet = db.GetSignUpSheet(em.SheetId);
SignUpEM newEM = Map.ToSignUpEM(signUpSheet);
foreach (var sourceField in em.Fields) {
FieldEM targetField = newEM.Fields.FirstOrDefault(f => f.FieldDefId == sourceField.FieldDefId && f.Id == sourceField.Id);
if (targetField.FieldType.IsEditable()) {
if (targetField.FieldType.IsBoolean()) {
((BooleanFieldEM)targetField).BooleanValue = ((BooleanFieldEM)sourceField).BooleanValue;
}
else {
targetField.Value = sourceField.Value;
}
targetField.SetFullyLoaded();
}
}
// This line is here to simulate an invalid value passed into an email field.
newEM.Fields[0].Value = "invalid";
newEM.SetFullyLoaded();
ModelState.Clear();
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(newEM, new ValidationContext(newEM, null, null), validationResults, true);
foreach (var result in validationResults) {
if (result == null) { continue; } // otherwise we need to avoid duplicates.
if (result.MemberNames.Count() == 0)
ModelState.AddModelError(String.Empty, result.ErrorMessage);
else
foreach (var name in result.MemberNames)
ModelState.AddModelError(name, result.ErrorMessage);
}
if (!ModelState.IsValid) {
return View(newEM);
}
SignUp su = Map.ToSignUp(em);
db.AddSignUp(su);
return RedirectToAction(MVC.SignUp.SignUp(em.SheetId));
}
Here's the validation code for SignUpEM...
private bool fullyLoaded;
internal void SetFullyLoaded() {
fullyLoaded = true;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (fullyLoaded) {
foreach (var f in Fields)
foreach (var vr in f.Validate(validationContext))
yield return vr;
if (ListIntro == null)
yield return new ValidationResult("ListIntro is required for now.");
}
}
And the validation code for FieldEM...
private bool fullyLoaded;
internal void SetFullyLoaded() {
fullyLoaded = true;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (fullyLoaded) {
if (Optional == false && String.IsNullOrEmpty(Value)) {
string message = string.Format("{0} is required.", DisplayName);
yield return new ValidationResult(message);
}
}
}

MVC-Create a ViewModel class instance from the urlreferrer url

Given:
request.UrlReferrer.LocalPath = "/MyApp/MyHome/List";
and I have a Route Mapping that handles this where MyHome is my controller and List is an action that takes a ViewModel. Other variations of this Route include paging and sorting but these are captured by the ViewModel.
My question is this:
How can I use the above URL to generate an instance of the related ViewModel?
EDIT: I have an JQuery Dialog that is adding/updating/deleting an item in a list that is shown by the url in the urlreferrer- the example given is the most basic. When the dialog sends the data to be a/u/d, I want to return the updated body of the list and display that. This information is handled by a different ViewModel than what is instantiated on the POST from the dialog (the url posted to is "/MyApp/MyHome/Edit/True" - for creating a new whatever). This piece follows the standard MVC process and of course works. What I want to do is create a second ViewModel based on the ViewModel for the list action and return this as a partial view containing the updated paged list.
Ok... I think I have this figured out. This is not pretty but it works. I welcome anybody's input to actually feed this through a ModelBinder or any other MVC artifact but here's what I came up with:
First we need to fake a request using the UrlReferrer instead of the actual url being requested:
public class FakeHttpContext : HttpContextBase
{
public FakeHttpContext(HttpContextBase currentContext)
{
_request = new FakeHttpRequest(currentContext.Request);
}
HttpRequestBase _request;
public override HttpRequestBase Request
{
get
{
return _request;
}
}
HttpResponseBase _response = new FakeHttpResponse();
public override HttpResponseBase Response
{
get
{
return _response;
}
}
class FakeHttpRequest : HttpRequestBase
{
HttpRequestBase _request;
public FakeHttpRequest(HttpRequestBase currentRequest)
{
if(currentRequest == null)
throw new ArgumentNullException();
this._request = currentRequest;
}
public override string ApplicationPath
{
get
{
return this._request.ApplicationPath;
}
}
public override string AppRelativeCurrentExecutionFilePath
{
get
{
return "~" + this._request.UrlReferrer.AbsolutePath.Remove(0, this._request.ApplicationPath.Length);
}
}
public override string PathInfo
{
get
{
return this._request.PathInfo;
}
}
}
class FakeHttpResponse : HttpResponseBase
{
}
}
Next, we feed the fake call through the RouteTable to get it broken down. and match up properties to the RouteData.Values.
public static class RouteAndModelBinder
{
public static void BuildViewModel<TViewModel>(ControllerContext context, TViewModel model)
{
FakeHttpContext fake = new FakeHttpContext(context.HttpContext);
RouteData test = RouteTable.Routes.GetRouteData(fake);
PropertyInfo[] properties = typeof(TViewModel).GetProperties();
string value;
foreach(PropertyInfo info in properties)
{
if(test.Values.ContainsKey(info.Name))
{
value = (string)test.Values[info.Name];
if(value == null)
{
continue;
}
if(info.PropertyType.IsGenericType &&
info.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type[] nullables = info.PropertyType.GetGenericArguments();
if(nullables.Length > 0)
{
Type nullableType = nullables[0];
if(nullableType.BaseType == typeof(Enum))
{
object o = Enum.Parse(nullableType, value);
info.SetValue(model, o, null);
}
else if(nullableType == typeof(Int32))
{
info.SetValue(model, int.Parse(value), null);
}
else
{
info.SetValue(model, Convert.ChangeType(value, info.PropertyType), null);
}
}
}
else
{
if(info.PropertyType.BaseType == typeof(Enum))
{
object o = Enum.Parse(info.PropertyType.BaseType, value);
info.SetValue(model, o, null);
}
else if(info.PropertyType == typeof(Int32))
{
info.SetValue(model, int.Parse(value), null);
}
else
{
info.SetValue(model, value, null);
}
}
}
}
}
}
Again, I welcome anybody's suggestions on how I can do this with already established MVC code (ie, ModelBinders, etc). I took some ideas and probably code from here (for the nullable type) and here.

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<RuleViolation>();
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.

asp.net mvc related, mainly a refactor question

can anyone think of a better way to do this?
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SaveAction()
{
NameValueDeserializer value = new NameValueDeserializer();
// selected messages
MemberMessageSaveAction[] messages = (MemberMessageSaveAction[])value.Deserialize(Request.Form, "value", typeof(MemberMessageSaveAction[]));
// selected action
MemberMessageAction action = (MemberMessageAction)Enum.Parse(typeof(MemberMessageAction), Request.Form["action"]);
// determine action
if (action != MemberMessageAction.MarkRead &&
action != MemberMessageAction.MarkUnRead &&
action != MemberMessageAction.Delete)
{
// selected action requires special processing
IList<MemberMessage> items = new List<MemberMessage>();
// add selected messages to list
for (int i = 0; i < messages.Length; i++)
{
foreach (int id in messages[i].Selected)
{
items.Add(MessageRepository.FetchByID(id));
}
}
// determine action further
if (action == MemberMessageAction.MoveToFolder)
{
// folders
IList<MemberMessageFolder> folders = FolderRepository.FetchAll(new MemberMessageFolderCriteria
{
MemberID = Identity.ID,
ExcludedFolder = Request.Form["folder"]
});
if (folders.Total > 0)
{
ViewData["messages"] = items;
ViewData["folders"] = folders;
return View("move");
}
return Url<MessageController>(c => c.Index("inbox", 1)).Redirect();
}
else if (action == MemberMessageAction.ExportXml)
{
return new MemberMessageDownload(Identity.ID, items, MemberMessageDownloadType.Xml);
}
else if (action == MemberMessageAction.ExportCsv)
{
return new MemberMessageDownload(Identity.ID, items, MemberMessageDownloadType.Csv);
}
else
{
return new MemberMessageDownload(Identity.ID, items, MemberMessageDownloadType.Text);
}
}
else if (action == MemberMessageAction.Delete)
{
for (int i = 0; i < messages.Length; i++)
{
foreach (int id in messages[i].Selected)
{
MemberMessage message = MessageRepository.FetchByID(id);
if (message.Sender.ID == Identity.ID || message.Receiver.ID == Identity.ID)
{
if (message.Sender.ID == Identity.ID)
{
message.SenderActive = false;
}
else
{
message.ReceiverActive = false;
}
message.Updated = DateTime.Now;
MessageRepository.Update(message);
if (message.SenderActive == false && message.ReceiverActive == false)
{
MessageRepository.Delete(message);
}
}
}
}
}
else
{
for (int i = 0; i < messages.Length; i++)
{
foreach (int id in messages[i].Selected)
{
MemberMessage message = MessageRepository.FetchByID(id);
if (message.Receiver.ID == Identity.ID)
{
if (action == MemberMessageAction.MarkRead)
{
message.ReceiverRead = true;
}
else
{
message.ReceiverRead = false;
}
message.Updated = DateTime.Now;
MessageRepository.Update(message);
}
}
}
}
return Url<MessageController>(c => c.Index("inbox", 1)).Redirect();
}
I think you can also leverage the mvc framework for most of your code. Correct me if I'm wrong because I'm gonna make a few assumptions about your classes because I can't deduct it from your post.
My assumptions:
Request.Form["action"] is a single value selectbox
Request.Form["value"] is a multy value selectbox
action is the kind of action you want to be taken on all the messages
message is the list of values that should go with the action
I would try to leverage the framework's functionality where possible
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SaveMemberAction(SelectList selectedMessages, MemberMessageAction actionType){
//Refactors mentioned by others
}
If you then give your inputs in your Html the correct name (in my example that would be selectedMessages and actionType) the first few rules become unnessecary.
If the default modelBinder cannot help you, you might want to consider putting the parsing logic in a custom modelbinder. You can search SO for posts about it.
As a side note: you might want to reconsider your variable namings. "action" might be confusing with MVC's action (like in ActionResult) and MemberMessageSaveAction might look like it's a value of MemberMessageAction enum. Just a thought.
The first step will be making different methods for each action.
Next is to remove the negative logic.
This results in something like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SaveAction() {
// SNIP
if (action == MemberMessageAction.Delete) {
return DoDeleteAction(...);
}
else if (action == MemberMessageAction.MoveToFolder) {
return DoMoveToFolderAction(...);
}
else if (action == MemberMessageAction.ExportXml) {
return DoExportXmlAction(...);
}
else if (action == MemberMessageAction.ExportCsv) {
return DoExportCsvAction(...);
}
else {
return HandleUnknownAction(...);
}
}
Turn MemberMessageAction into a class that has a Perform virtual function.
For your Special actions, group the common Perform code:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SaveAction()
{
NameValueDeserializer value = new NameValueDeserializer();
MemberMessageSaveAction[] messages = (MemberMessageSaveAction[])value.Deserialize(Request.Form, "value", typeof(MemberMessageSaveAction[]));
MemberMessageAction action = MemberMessageAction.FromName(
messages,
Request.Form["action"]));
return action.Perform();
}
class MoveToFolder : SpecialAction { /*...*/ }
class ExportXml : SpecialAction { /*...*/ }
class ExportCsv : SpecialAction { /*...*/ }
class Delete : MemberMessageAction { /*...*/ }
class MarkRead : MemberMessageAction { /*...*/ }
class MarkUnRead : MemberMessageAction { /*...*/ }
abstract class MemberMessageAction {
protected MemberMessageSaveAction[] messages;
public MemberMessageAction(MemberMessageSaveAction[] ms) { messages = ms; }
public abstract ActionResult Perform();
public static MemberMessageAction FromName(MemberMessageSaveAction[] ms, string action) {
// stupid code
// return new Delete(ms);
}
}
abstract class SpecialAction : MemberMessageAction {
protected IList<MemberMessage> items;
public SpecialAction(MemberMessageSaveAction[] ms) : base(ms) {
// Build items
}
}
Now you can easily factor the code.
I don't like
MessageRepository.FetchByID(messages[i].ID)
this will make messages.Length (selected) queries to the database. I think you need to store your messages in ViewData, perform a filtering and pass them to Update() without the need to requery your database.
I came up with this.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Update(MemberMessageUpdate[] messages, MemberMessage.Action action)
{
var actions = new List<MemberMessage.Action>
{
MemberMessage.Action.MoveToFolder,
MemberMessage.Action.ExportCsv,
MemberMessage.Action.ExportText,
MemberMessage.Action.ExportText
};
if (actions.Contains(action))
{
IList<MemberMessage> items = new List<MemberMessage>();
for (var i = 0; i < messages.Length; i++)
{
if (messages[i].Selected == false)
{
continue;
}
items.Add(MessageRepository.FetchByID(messages[i].ID));
}
if (action == MemberMessage.Action.MoveToFolder)
{
var data = new MessageMoveViewData
{
Messages = items
};
return View("move", data);
}
return new MessageDownloadResult(Identity.ID, items, action);
}
MessageRepository.Update(messages, action);
return Url<MessageController>(c => c.Index(null, null, null, null)).Redirect();
}

Resources