Getting values for a Model property from a Web API call - c#

Currently I'm populating dropdowns options using an API call. In my model I have something like this:
public List<SelectListItem> getOptions
{
get
{
var options = WebApiHelper.Download<IEnumerable<T>>(
"Controller");
var dropDownOptions = options.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.Value }).ToList();
return dropDownOptions;
}
}
And this is called in a couple of places in the .cshtml (e.g. see below):
#Html.DropDownListFor(m => m.someProperty, Model.getOptions)
List<SelectListItem> GetDropdownOptions()
{
var currentDropdownItems = Model.getOptions;
//some other code to modify dropdown options.
}
Will the Web API only be hit once when I call Model.getOptions? Or will it be called every time since it is inside the get for that property? If it is the latter, what is a good work around for that issue?
Edit: thought about it a little more, would it be better to have these controller populate the values for that property? I have a feeling that an api call gets placed on every call to model.getoptions.

You WebApiHelper.Download() will be called each time you access the property and, if you were editing a collection, your current implementation could seriously degrade performance.
While you could define a private field (say) private List<SelectListItem> _Options; and in the getter, check if its null and if so, populate it using your WebApiHelper.Download() method, and then return _Options; this is still not a preferred implementation since you cannot easily unit test your app.
private List<SelectListItem> _options;
public List<SelectListItem> getOptions
{
get
{
if (_options == null)
{
_options = // call your service
}
return _options;
}
}
Keep your view models as dumb as possible and make your property
public List<SelectListItem> Options { get; set; }
and in the controller, initialize your model and call WebApiHelper.Download() to populate the collection (and inject WebApiHelper into your controller)

Related

ASP.NET MVC - Best practices when passing dependencies

I have a question, that I tried to Google but honestly, I don't really know how to search or even ask this particular question.
Let's imagine I have the following:
Controller
[HttpGet]
public virtual ActionResult Summary()
{
var summaryViewModel = new CheckoutSummaryViewModel()
{
Products = ProductViewModel.BuildListFromShoppingCart(GetShoppingCart())
};
return View("_Summary", summaryViewModel);
}
ProductViewModel
public class ProductViewModel
{
public string Name
{
get; set;
}
public static List<ProdutoCheckoutViewModel> BuildListFromShoppingCart(ShoppingCart shoppingCart, IMappingService mappingService)
{
var itemsInCart = new List<ProductViewModel>();
foreach (var item in shoppingCart.ItemsInCart)
{
var itemViewModel = mappingService.Map<Product, ProductViewModel>(item.Product);
itemViewModel.Quantidade = item.Quantity;
itemsInCart.Add(itemViewModel);
}
return itemsInCart;
}
}
This is not production code. Is just so I can explain what I mean.
Is this the correct way of doing this?
Is there a better way than using static for building the list? I really don't want to do it inside the controller.
Passing IMappingService to the method does not look right. But maybe I'm just being picky. Is it the best way?
Another case, where I need to pass Session State to a static helper class.
public static Guid GetCheckoutId(HttpSessionStateBase session)
{
return (Guid)session["checkoutId"];
}
Or, also, sometimes I need to pass as parameter, to helper methods, my "unifOfWork", since I use the repository pattern.
I've come accross this "problem" a lot and I did not find the best way, yet, to do it.
PS: If any of you has a better title for this question, please tell me so I can update it.
Controller
If you use DI, it would look something like this:
public class CheckoutController
{
private readonly ICheckoutService _checkoutService;
public CheckoutController(ICheckoutService checkoutService) =>
_checkoutService = checkoutService;
[HttpGet]
public virtual ActionResult Summary()
{
var shoppingCartData = _checkoutService.GetShoppingCart(Session["..."]);
// The viewmodel here could be returned by your service or the service
// would return all required data and the viewmodel simply transforms that Dto into what is needed by the UI
var summaryViewModel = new CheckoutSummaryViewModel()
{
Products = shoppingCartData
};
return View("_Summary", summaryViewModel);
}
}

ASP.NET MVC Razor View built on Viewmodel of two objects. Architecture advice How to update a calculated field referencing both objects

I working in an ASP.NET MVC5 project and have a viewmodel class called PlacementStudentIndexData which has four collections of Ienumerables,
The viewmodel class is declared as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Placementv2.Models;
using IdentitySample.Models;
namespace Placementv2.ViewModels
{
public class PlacementStudentIndexData
{
public IEnumerable<ApplicationUser> UserChoice1 { get; set; }
public IEnumerable<ApplicationUser> UserChoice2 { get; set; }
public IEnumerable<ApplicationUser> UserChoice3 { get; set; }
public IEnumerable<Placement> Placement { get; set; }
public double distance { get; set; }
}
Note the ApplicationUser is uses the user objects on the objects in Identity2.0.
The Placements Controller has a Details Action to pass ViewModel data to a razor view built on this ViewModel which performs a matching exercise between the two objects and returns three matched collections of Users and an collection with a single instance of Placement type based on the int value passed to the Action to indicate which Placement object to return)
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var viewModel = new PlacementStudentIndexData();
viewModel.Placement = db.Placements.Where(p => p.PlacementID== id.Value);
viewModel.UserChoice1 = db.Users.Where(p => p.Choice1 == viewModel.Placement.FirstOrDefault().PlacementOrganisation.PlacementTypeID).Where(p => p.Placed == false);
viewModel.UserChoice2 = db.Users.Where(p => p.Choice2 == viewModel.Placement.FirstOrDefault().PlacementOrganisation.PlacementTypeID).Where(p => p.Placed == false);
viewModel.UserChoice3 = db.Users.Where(p => p.Choice3 == viewModel.Placement.FirstOrDefault().PlacementOrganisation.PlacementTypeID).Where(p => p.Placed == false);
return View(viewModel);
Elsewhere in my Placement controller I have the following method and am looking for advice on how to update this distance into the individual user instances as calculated against the current placement instance. The calculation should be based on passing geocoordinates which are already stored in the database for the User object and the PlacementOrganisation (which is associated to the Placement)
public double CalculateDistance(double startlat, double startlong, double endlat, double endlong)
{
[Google MAPS API calculation and JSON deserialisation code here]
return distancekms;
}
My difficulty is where in my code to perform these calculations and I'm looking for advice on how to update the distance attribute for the existing three UserChoice1,UserChoice2, UserChoice3 collections (As a side note, I know these are ienumerables, is it wrong to describe them as collections?)
If so could someone give me an example of how I would extend out my Details controller action to include a call to this CalculateDistance method so that the updated distance attribute for each User instance is part of the ViewModel passed to the Razor View.
Note- I've already tried include a foreach block in the controller method which called the CalculateDistance method along the following lines
foreach (var item in viewModel.UserChoice1)
{
viewModel.UserChoice1.First().distance = Calculatedistance(viewModel.Placement.First().PlacementOrganisation.Latitude, viewModel.Placement.First().PlacementOrganisation.Longtitude, viewModel.UserChoice1.First().Latitude, viewModel.UserChoice1.First().Longtitude);
}
However, it's throwing the following exception:
"There is already an open DataReader associated with this Command which must be closed first."
Please let me know if you can see a way to achieve what I'm trying to do here within the Details action of my Placement controller. Alternatively, should I be looking to call this CalculateDistance method from the view as I iterate through the User objects, or performing the calculation via Javascript in the View itself.
Thanks in advance for any help/advice you can offer on this.
The Link that #Dandy provided did contain the solution in one of the lower voted answers on that link.
The solution is to append .ToList() to the Ienumerables and that seems to close the DataReader properly.
This eventually got me past the issue where my foreach loop was throwing an error
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var viewModel = new PlacementStudentIndexData();
viewModel.Placement = db.Placements.Where(p => p.PlacementID == id.Value).ToList();
viewModel.User = db.Users.Where(p => p.Placed == false).OrderBy(p=>p.distance).ToList();
foreach (ApplicationUser user in viewModel.User)
{
user.distance = Calculatedistance(user.Latitude,user.Longtitude, viewModel.Placement.FirstOrDefault().PlacementOrganisation.Latitude, viewModel.Placement.FirstOrDefault().PlacementOrganisation.Longtitude);
}
return View(viewModel);
}

How can I return an empty list instead of a null list from a ListBoxFor selection box in Asp.net MVC?

controller get action
IList<InputVoltage> inputVoltagesList = unitOfWorkPds.InputVoltageRepository.GetAll.ToList();
pdsEditViewModel.InputVoltageList = inputVoltagesList.Select(m => new SelectListItem { Text = m.Name, Value = m.Id.ToString() });
ViewModel
public IEnumerable<SelectListItem> InputVoltageList { get; set; }
public List<int> SelectedInputVoltages { get; set; }
View
#Html.ListBoxFor(m => m.SelectedInputVoltages, Model.InputVoltageList)
I want to receive a null list when a user makes no selections the selectedInputvoltages comes into my post controller action as null how do I get it to come in as an empty list?
I like both answers is there any benefit in using one over the other?
Either make sure it is initialized in the controller (or model/viewmodel) if null, or perhaps (ugly code though) use the coalesce operator to initialize on the fly if null:
#Html.ListBoxFor(m => m.SelectedInputVoltages, Model.InputVoltageList ?? new List<SelectListItem>())
If you initialize the list in the view model's constructor then it will always be at least an empty list. Anything which builds an instance of the view model would continue to set the list accordingly.
public class SomeViewModel
{
public List<int> SelectedInputVoltages { get; set; }
public SomeViewModel()
{
SelectedInputVoltages = new List<int>();
}
}
This way it will never be null in an instance of SomeViewModel, regardless of the view, controller, etc.
If you always want the view model's property to have a default value, then the best place to put that is in the view model. If that logic is instead placed in the controller or the view then it would need to be repeated any time you want to use it.

Where should SelectListItem lists go?

In an MVC application, I have some DropDownLists. In my controller I create the IEnumerable<SelectListItem>s and transfer them to my View. Unfortunately, if there is a validation error, I need to recreate those lists, otherwise the view rendering fails.
In the controller action method I'm doing:
var possibilities = _repo.Objects.Select(o=>new SelectListItem(...));
viewmodel.Possibilities = possibilities;
return View(viewmodel);
The view-model has the Possibilities property defined like this:
IEnumerable<SelectListItem> Possibilities { get; set; }
And in my view I access it:
#Html.DropDownListFor(vm=>vm.ThePossibility, vm.Possibilities)
The problem is that when the form post action method is called, the view model passed to it has a null for Possibilities, so when I call:
if(!ModelState.IsValid)
return View(model);
The view doesn't get rendered.
I understand why the propery is null on the post action method, but what's the best way of correcting this? I'd rather not reinitialize those lists.
Thanks.
If you don't want to re-initialize the lists, you will have to cache them somewhere, such as the session or somewhere else.
Frankly, in most cases, it's just simpler to rebuild them. You will have to re-assign them every time.
You should look into using the Post-Redirect-Get pattern; There is a nice pair of attributes described in this blog post that make this very easy to do in ASP.Net MVC.
I usually cache these somewhere or provide a static class for getting common lists. You can then provide access to these in your model with a getter.
For example:
IEnumerable<SelectListItem> _possibilities
IEnumerable<SelectListItem> Possibilities
{
get
{
if (_possibilities == null)
_possibilities = CommonLists.Possibilities();
return possibilities;
}
}
Accessors and JSON (NetwonSoft) are your friends.
In a nutshell
When you set the IEnumerable<SelectListItem> property of your model, serialize it to a public string property.
When your public string property is being set, and the IEnumerable<SelectListItem> is not defined (e.g. null), deserialize the string property to the IEnumerable<SelectListItem> property of your model.
In your view, embed the serialized string so that it is persisted between posts to the server. (Eg. #Html.HiddenFor(...))
Model
public class MyViewModel
{
public IEnumerable<SelectListItem> Selections
{
get
{
return selections;
}
set
{
selections= value;
// serialize SelectListItems to a json string
SerializedSelections = Newtonsoft.Json.JsonConvert.SerializeObject(value);
}
}
IEnumerable<SelectListItem> selections;
public string SerializedSelections
{
get
{
return serializedSelections;
}
set
{
serializedSelections = value;
if(Selections == null)
{
// SelectListItems aren't defined. Deserialize the string to the list
Selections = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<SelectListItem>>(value);
}
}
}
string serializedSelections;
}
Razor View
<form>
...
#Html.HiddenFor(m => m.SerializedSelections)
...
#Html.DropDownListFor(m => m.SomeProperty, Model.Selections)
</form>

Pass tuple with two object collections to view?

I want to pass two collections of objects. First is Post, second is Gallery. However I get error and I don't know how to fix this.
I've done this when passing two single objects and it is working fine, but now I need to pass two collections of those objects and it gives me error.
Error
The model item passed into the dictionary is of type
'System.Tuple2[System.Linq.IQueryable1[photoBlog.Models.Gallery],System.Linq.IQueryable1[photoBlog.Models.Post]]',
but this dictionary requires a model item of type
'System.Tuple2[System.Collections.Generic.IEnumerable1[photoBlog.Models.Gallery],System.Collections.Generic.IEnumerable1[photoBlog.Models.Post]]'.
Controller
public ActionResult Index()
{
photoBlogModelDataContext _db = new photoBlogModelDataContext();
var posts = _db.Posts.OrderByDescending(x => x.DateTime).Take(4);
var galleries = _db.Galleries.OrderByDescending(x => x.ID).Take(4);
return View(Tuple.Create(galleries, posts));
}
View
#model Tuple<IEnumerable<photoBlog.Models.Gallery>, IEnumerable<photoBlog.Models.Post>>
#foreach (var item in Model.Item1)
{
#item.Name
}
I think you should modify your controller method to this:
public ActionResult Index()
{
photoBlogModelDataContext _db = new photoBlogModelDataContext();
var posts = _db.Posts.OrderByDescending(x => x.DateTime).Take(4).ToArray();
var galleries = _db.Galleries.OrderByDescending(x => x.ID).Take(4).ToArray();
return View(Tuple.Create(galleries, posts));
}
From your error message, it appears that the queries are not resolved yet when your view is rendered. By also doing ToArray or ToList, you will force the query to hit the database before you return from the controller method.
You need to create a new Entity in your domain.Model
Here is an example I provided today :
MVC3: button to send both form (model) values and an extra parameter
Later Edit :
namespace App.Domain.Model
{
public class Tuple
{
public IEnumerable<photoBlog.Models.Gallery> Gallery{ get; set; }
public IEnumerable<photoBlog.Models.Post> Post{ get; set; }
}
}
you will send the Tuple object to your view as follows :
#model: (Domain.Model.)Tuple <--- (your exact model path )
#foreach (var item in Model.Gallery)
{
#item.Name
}
Another posibility would be using viewbag ... but i strongly suggest you do it the correct way,i.e. the MVC way
Just try this
var posts = _db.Posts.OrderByDescending(x => x.DateTime).Take(4).ToList();
var galleries = _db.Galleries.OrderByDescending(x => x.ID).Take(4).ToList();

Categories

Resources