asp.net mvc related, mainly a refactor question - asp.net-mvc

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();
}

Related

ASP.NET Core [FromBody] vs MVC 5 binding

I got an MVC 5 application that i'm porting to asp.net Core.
In the MVC application call to controller we're made using AngularJS $resource (sending JSON) and we we're POSTing data doing :
ressource.save({ entries: vm.entries, projectId: vm.project.id }).$promise...
that will send a JSON body like:
{
entries:
[
{
// lots of fields
}
],
projectId:12
}
the MVC controller looked like this :
[HttpPost]
public JsonResult Save(List<EntryViewModel> entries, int projectId) {
// code here
}
How can I replicate the same behaviour with .NET Core since we can't have multiple [FromBody]
you cannot have multiple parameter with the FromBody attibute in an action method. If that is need, use a complex type such as a class with properties equivalent to the parameter or dynamic type like that
[HttpPost("save/{projectId}")]
public JsonResult Save(int projectId, [FromBody] dynamic entries) {
// code here
}
As pointed out in the comment, one possible solution is to unify the properties you're posting onto a single model class.
Something like the following should do the trick:
public class SaveModel
{
public List<EntryViewModel> Entries{get;set;}
public int ProjectId {get;set;}
}
Don't forget to decorate the model with the [FromBody] attribute:
[HttpPost]
public JsonResult Save([FromBody]SaveViewModel model)
{
// code here
}
Hope this helps!
It's still rough but I made a Filter to mimic the feature.
public class OldMVCFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Method != "GET")
{
var body = context.HttpContext.Request.Body;
JToken token = null;
var param = context.ActionDescriptor.Parameters;
using (var reader = new StreamReader(body))
using (var jsonReader = new JsonTextReader(reader))
{
jsonReader.CloseInput = false;
token = JToken.Load(jsonReader);
}
if (token != null)
{
var serializer = new JsonSerializer();
serializer.DefaultValueHandling = DefaultValueHandling.Populate;
serializer.FloatFormatHandling = FloatFormatHandling.DefaultValue;
foreach (var item in param)
{
JToken model = token[item.Name];
if (model == null)
{
// try to cast the full body as the current object
model = token.Root;
}
if (model != null)
{
model = this.RemoveEmptyChildren(model, item.ParameterType);
var res = model.ToObject(item.ParameterType, serializer);
context.ActionArguments[item.Name] = res;
}
}
}
}
}
private JToken RemoveEmptyChildren(JToken token, Type type)
{
var HasBaseType = type.GenericTypeArguments.Count() > 0;
List<PropertyInfo> PIList = new List<PropertyInfo>();
if (HasBaseType)
{
PIList.AddRange(type.GenericTypeArguments.FirstOrDefault().GetProperties().ToList());
}
else
{
PIList.AddRange(type.GetTypeInfo().GetProperties().ToList());
}
if (token != null)
{
if (token.Type == JTokenType.Object)
{
JObject copy = new JObject();
foreach (JProperty jProp in token.Children<JProperty>())
{
var pi = PIList.FirstOrDefault(p => p.Name == jProp.Name);
if (pi != null) // If destination type dont have this property we ignore it
{
JToken child = jProp.Value;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, pi.PropertyType);
}
if (!IsEmpty(child))
{
if (child.Type == JTokenType.Object || child.Type == JTokenType.Array)
{
// nested value has been checked, we add the object
copy.Add(jProp.Name, child);
}
else
{
if (!pi.Name.ToLowerInvariant().Contains("string"))
{
// ignore empty value when type is not string
var Val = (string)child;
if (!string.IsNullOrWhiteSpace(Val))
{
// we add the property only if it contain meningfull data
copy.Add(jProp.Name, child);
}
}
}
}
}
}
return copy;
}
else if (token.Type == JTokenType.Array)
{
JArray copy = new JArray();
foreach (JToken item in token.Children())
{
JToken child = item;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, type);
}
if (!IsEmpty(child))
{
copy.Add(child);
}
}
return copy;
}
return token;
}
return null;
}
private bool IsEmpty(JToken token)
{
return (token.Type == JTokenType.Null || token.Type == JTokenType.Undefined);
}
}

How to stop code execute on condition from another controller?

my code:
public class StateManagementController : Controller
{
public void OwnerTest()
{
if (Session["Owner"] == null)
{
Logowanie();
}
}
public ActionResult Logowanie()
{
return RedirectToAction("Log", "Owner");
}
}
public class AnimalController : StateManagementController
{
public ActionResult MyAnimals()
{
OwnerTest();
//some code here
return View(animals.ToList());
}
}
The problem is that even if the session is null and Redirect is reached it doesn't redirect me but it still goes to ,,some code" in MyAnimals action, how can I stop it in ActionResult Logowanie? i dont want to change code in MyAnimals, I want only to use function there without checking if it returns something.
You Logowanie Action may return an ActionResult but the OwnerTest method ignores the returned result. Try this:
public class StateManagementController : Controller
{
public ActionResult OwnerTest()
{
if (Session["Owner"] == null)
{
return Logowanie();
}
else
{
return null;
}
}
public ActionResult Logowanie()
{
return RedirectToAction("Log", "Owner");
}
public class AnimalController : StateManagementController
{
public ActionResult MyAnimals()
{
var temp = OwnerTest();
if (temp != null)
{
return temp;
}
else
{
//some code here
return View(animals.ToList());
}
}
}

How to use Controller.TryUpdateModel outside of the Controller context?

is it possible to use Controller.TryUpdateModeloutside of the Controller context?
For Example suppose that I want to use Controller.TryUpdateModel method in class which does not belong to MyApp.Controllers so how that could be done?.
this is my code.
public ActionResult ValidateAndSignUp(company newCompany)
{
if (ModelState.IsValid)
{
newCompany.CDTO.File = Request.Files["Logo"];
if(new CompanyActions().AddCompany(newCompany))
{
ViewBag.message = newCompany.CDTO.Message;
return View(newCompany.CDTO.RedirectTo);
}
else
{
if (newCompany.CDTO.HasFileError)
{
ModelState.AddModelError("Logo", "Invalid File");
return View(newCompany.CDTO.RedirectTo, newCompany);
}
else
{
ViewBag.error = newCompany.CDTO.Error;
return View(newCompany.CDTO.RedirectTo);
}
}
}
else
{
newCompany.CDTO.Countries = new DropDownLists().GetAllCountries();
return View("signUp", newCompany);
}
}

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.

How to handle MVC String arrays in a RedirectToAction?

If I have the following:
[HttpPost]
public ActionResult DeclineClaims(String button, String[] Decline)
{
if (button == "claim")
{
return RedirectToAction("NewExpense", "Claim", new { Create = Decline });
}
....
....
}
and receive it via the RedirectToAction here:
public ActionResult NewExpense(String[] Create)
{
...
}
'Create' in the second action is an empty string. This problem does not occur with standard Int and Strings.
How should I handle the String array?
You may try this:
[HttpPost]
public ActionResult DeclineClaims(String button, String[] Decline)
{
if (button == "claim")
{
var parameters = new RouteValueDictionary();
for (int i = 0; i < decline.Length; i++)
{
parameters["Create[" + i + "]"] = Decline[i];
}
return RedirectToAction("NewExpense", parameters);
}
....
....
}
Sender:
[HttpPost]
public ActionResult DeclineClaims(String button, String[] Decline)
{
if (button == "claim")
{
TempData["Create"] = Decline;
return RedirectToAction("NewExpense", "Claim");
}
....
....
}
Receiver:
public ActionResult NewExpense()
{
String[] data = (String[])TempData["Create"];
}

Resources