Getting error on constructors using reactiveui - c#

I have a view model that has two view models as properties:
public class PokemonViewModel : ReactiveObject, IEnableLogger
{
private readonly IPokemonService PokemonService;
[Reactive]
public PokemonListViewModel PokemonListViewModel { get; set; }
private readonly ObservableAsPropertyHelper<PokemonDetailsViewModel> pokemonDetailsViewModel;
public PokemonDetailsViewModel PokemonDetailsViewModel => pokemonDetailsViewModel.Value;
public PokemonViewModel(IPokemonService pokemonService = null)
{
PokemonService = pokemonService ?? Locator.Current.GetService<IPokemonService>();
PokemonListViewModel = new PokemonListViewModel();
pokemonDetailsViewModel = this.WhenAnyValue(x => PokemonListViewModel.SelectedPokemon)
.SelectMany(GetPokemonDetailsViewModelAsync)
.ToProperty(this, x => x.PokemonDetailsViewModel);
}
//private methods...
}
The PokemonListViewModel contains a SelectedPokemon property which is not initialized on constructor, so it's null at startup
Given this, when I run the application, I get the following error: "Unsupported expression of type 'Constant'. Did you miss the member access prefix in the expression?"
How can I solve this?

It should be x => x.PokemonListViewModel in the WhenAnyValue call.

Related

How to Provide Fluent Validation on Multi-Select Component using MudBlazor

I have a Blazor app that manages a lot of form input. Every form is tied to an instance of type IncidentLog, and every UI element of the form is tied to a property of that IncidentLog instance.
In the form, we have a MudSelect component where T="Department". The MudSelect has MultiSelection="true", and the results are stored in a IEnumerable(Department) Departments property of the IncidentLog instance.
This component works totally fine, but I've tried implementing FluentValidation in the form and I'm not sure how to define the expression of the For parameter of this MudSelect component. It looks like it's expecting me to pass an object that matches T="Department", but the validation I need to run against the validator is based off of IEnumerable(Department)... I just want to check that the user has selected at least one department from the multiselect component.
Error message when trying to pass IEnumerable object to the For validator:
IncidentLogPropertiesComponent razor page - relevant blurb
<MudForm Model="#Log" #ref="#form" Validation="#(incidentLogValidator.ValidateValue)" ValidationDelay="100">
<MudCardContent Class="pt-0">
...
<MudSelect T="Department" ToStringFunc="#ConverterDepartment" Dense="true" Margin="Margin.Dense" Label="Affected Departments:" MultiSelection="true" #bind-SelectedValues="Log.Departments" Clearable>
#foreach (var department in Departments.OrderBy(o=>o.DepartmentName).ToList())
{
<MudSelectItem Value="#department"/>
}
</MudSelect>
...
</MudCardContent>
</MudForm>
IncidentLog.cs:
public class IncidentLog : Log
{
[Key]
public int IncidentID { get; set; }
....
[Write(false)]
public IEnumerable<Department> Departments { get; set; } = new HashSet<Department>();//3
....
}
IncidentLogPropertiesComponentBase
public class IncidentLogPropertiesComponentBase : OwningComponentBase<iIncidentLogRepository>, IDisposable
{
protected IncidentLogValidator incidentLogValidator = new IncidentLogValidator();
...
[Parameter]
public IncidentLog? Log
{
get
{
return (AppState.SelectedLog != null && AppState.SelectedLog.LogType.LogTypeID == 2) ? (IncidentLog)AppState.SelectedLog : null;
}
set
{
AppState.SetLog(value);
}
}
}
IncidentLogValidator:
public class IncidentLogValidator: AbstractValidator<IncidentLog>
{
public IncidentLogValidator()
{
...
RuleFor(t => t.Departments).NotEmpty().WithMessage("Must enter at least one department affected.");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<IncidentLog>.CreateWithOptions((IncidentLog)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
*I didn't link my AppState state management file even though it's referenced in code above - I don't think it's relevant to the issue I'm having.
How do I provide Fluent Validation to Multi-Select Dropdowns using MudBlazor?

AutoMapping Object with Constructor Arguments

I am trying to configure AutoMapper to work with a class which requires a constructor argument. I've done quite a bit of online research into this, and found several examples...but they either don't use instance-based AutoMapper, or they are for older versions of AutoMapper.
I'm using AutoMapper to deep clone objects. Here's the profile I've created:
public class ScannerAutoMapProfile : Profile
{
public ScannerAutoMapProfile()
{
CreateMap<AzureConfiguration>();
CreateMap<CommunityUser>();
CreateMap<Community>();
CreateMap<Contact>();
CreateMap<DatabaseConnection>();
CreateMap<DatabaseConfiguration>();
CreateMap<LoginManagement>();
CreateMap<MaxLoadSeconds>();
CreateMap<ScanTime>();
CreateMap<ScanningAcceleration>();
CreateMap<ScanningWindow>();
CreateMap<ScanningInterval>();
CreateMap<Scanning>();
CreateMap<SearchParameterUser>();
CreateMap<SearchParameter>();
CreateMap<ScannerConfiguration>();
}
private void CreateMap<TModel>()
{
CreateMap<TModel, TModel>();
}
}
The problem is occurring with ScannerConfiguration, which takes a single string parameter.
Here's the Autofac module I'm using:
public class AutoMapperModule : Module
{
protected override void Load( ContainerBuilder builder )
{
base.Load( builder );
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
builder.RegisterAssemblyTypes( assemblies )
.Where( t => typeof(Profile).IsAssignableFrom( t ) && !t.IsAbstract && t.IsPublic )
.As<Profile>();
builder.Register( c => new MapperConfiguration( cfg =>
{
foreach( var profile in c.Resolve<IEnumerable<Profile>>() )
{
cfg.AddProfile( profile );
}
} ) )
.AsSelf()
.SingleInstance();
builder.Register( c => c.Resolve<MapperConfiguration>().CreateMapper( c.Resolve ) )
.As<IMapper>()
.SingleInstance();
}
}
(thanx to http://www.protomatter.co.uk/blog/development/2017/02/modular-automapper-registrations-with-autofac/ for this).
I can successfully instantiate an IMapper, which shows the Autofac stuff is working properly. But when I attempt to call Map:
_onDisk = Mapper.Map<ScannerConfiguration>( _inMemory );
it fails with a "ScannerConfiguration has no parameterless constructor" exception.
Unfortunately, while I'm pretty sure I need to provide some options to that Map() call, I haven't been able to figure out how to do it. The parameter I need to pass in to the constructor is a public property of ScannerConfiguration, called SourceFilePath.
Since ScannerConfiguration requires a parameter, why don't you initialize it yourself?
var sc = new ScannerConfiguration("some string value");
_onDisk = Mapper.Map( _inMemory, sc );
If Automapper can't create an instance of the destination using a default constructor, you can give Automapper a function that calls a constructor and returns the new instance with ConstructUsing. After it constructs the object it continues mapping as usual.
For example here's a source and destination class. The destination class can't be created without calling a non-default constructor:
public class SourceClass
{
public string SourceStringProperty { get; set; }
public int OtherSourceProperty { get; set; }
public bool SameNameInBoth { get; set; }
}
public class DestinationClass
{
public DestinationClass(string destinationString)
{
DestinationStringPropertyFromConstructor = destinationString;
}
public string DestinationStringPropertyFromConstructor { get; }
public int OtherDestinationProperty { get; set; }
public bool SameNameInBoth { get; set; }
}
The mapping would look like this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SourceClass, DestinationClass>()
.ConstructUsing(hasString => new DestinationClass(hasString.SourceStringProperty))
.ForMember(dest => dest.OtherDestinationProperty,
opt => opt.MapFrom(src => src.OtherSourceProperty));
});
I added the other properties just to demonstrate that other than specifying a constructor, the other configuration details are all the same.

Type not found in cache

I'm trying to put an universal app together and I'm using mvvm light but I'm getting the following error when compiling my app:
Error 1 Type not found in cache: MyApp.Model.LocationModel
...\MyApp.WindowsPhone\Views\LocationPage.xaml 10 5 MyApp.WindowsPhone
It does compile successfully but I can't figure out what's causing the problem. I've found a couple of article on stackoverflow:
SimpleIoC - Type not found in cache: Windows.UI.Xaml.Controls.Frame
MVVM Light “Type Not Found in cache”
But neither one apply to my problem. The first thing I've noticed is that the error is somehow displaying a Model where the problem resides rather than a ViewModel.
Error 1 Type not found in cache: MyApp.Model.LocationModel.
...\MyApp\MyApp.WindowsPhone\Views\LocationPage.xaml 10 5 MyApp.WindowsPhone
The error in my xaml occurs on the line where I defined my DataContext:
<Page
....
DataContext="{Binding Source={StaticResource Locator}, Path=LocationViewModel}">
My LocationViewModel class is defined as follows:
public class LocationViewModel : ViewModelBase
{
private RelayCommand _saveCommand;
private RelayCommand _cancelCommand;
#region Properties
public int Id
{
get
{
return this.Location.Id;
}
}
public string Title
{
get
{
return this.Location.Title;
}
}
public string Description
{
get
{
return this.Location.Description;
}
}
public string CreatedDateFormatted
{
get
{
return this.Location.CreatedDate.ToString("d");
}
}
public string LastUpdatedDateFormatted
{
get
{
return Location.LastUpdatedDate.ToString("d");
}
}
public string ImagePath
{
get
{
return this.Location.ImagePath;
}
}
public LocationModel Location
{
get;
private set;
}
#endregion
#region Constructors
public LocationViewModel(LocationModel model)
{
this.Location = model;
this.Location.PropertyChanged += (s, e) =>
{
if (e.PropertyName == LocationModel.DescriptionPropertyName)
{
RaisePropertyChanged(() => Description);
}
if (e.PropertyName == LocationModel.TitlePropertyName)
{
RaisePropertyChanged(() => Title);
}
if (e.PropertyName == LocationModel.ImagePathPropertyName)
{
RaisePropertyChanged(() => ImagePath);
}
if (e.PropertyName == LocationModel.CreatedDateStringPropertyName)
{
RaisePropertyChanged(() => CreatedDateFormatted);
}
if (e.PropertyName == LocationModel.LastUpdatedDateStringPropertyName)
{
RaisePropertyChanged(() => LastUpdatedDateFormatted);
}
};
}
#endregion
public RelayCommand SaveCommand
{
get
{
return this._saveCommand ?? (this._saveCommand = new RelayCommand(ExecuteSaveCommand));
}
}
public RelayCommand CancelCommand
{
get
{
return this._cancelCommand ?? (this._cancelCommand = new RelayCommand(ExecuteCancelCommand));
}
}
private void ExecuteSaveCommand()
{
}
private void ExecuteCancelCommand()
{
}
}
and my property for my LocationViewModel is defined as follows in my ViewModelLocator class:
public LocationViewModel LocationViewModel
{
get
{
return ServiceLocator.Current.GetInstance<LocationViewModel>();
}
}
and is registered in the ViewModelLocator's constructor:
SimpleIoc.Default.Register<LocationViewModel>();
and when this code is called, it registers my LocationViewModel correctly.
When click on my "add" button, it navigate to the page where the LocationViewModel is set as the DataContext and the error occurs at run-time.
The code I'm calling from LocationsViewModel (not LocationViewModel) that's calling the navigation is:
private void ExecuteAddCommand()
{
_navigationService.Navigate(typeof(LocationPage));
}
When debugging the above, it creates the LocationPage, followed by calling the LocationViewModel from the ViewModelLocator and this is when the same error occurs but at run-time i.e.
return ServiceLocator.Current.GetInstance<LocationViewModel>();
When I move my mouse over the , it displays the following:
Message: "Type not found in cache: MyApp.Model.LocationModel."
InnerException: at GalaSoft.MvvmLight.Ioc.SimpleIoc.DoGetService
(Type serviceType, String key) at
GalaSoft.MvvmLight.Ioc.SimpleIoc.GetInstance[TService]()
at Inventory.ViewModel.ViewModelLocator.get_LocationViewModel()
Actually, I've just realized that the error is generated much earlier but no error is thrown. It is actually generated when registering the LocationViewModel in the constructor of ViewModelLocator:
SimpleIoc.Default.Register<LocationViewModel>();
Any ideas?
Thanks.
The LocationViewModel constructor has dependency on LocationModel. The SimpleIoc container couldn't create the view model instance as the constructor requires a LocationModel object which you can't pass directly. You can probably use MVVMLight Messenger to decouple the LocationModel object from the LocationViewModel constructor.
public LocationViewModel()
{
MessengerInstance.Register<LocationModel>(this,m=>{model=m;
//PropertyChanged code
});
}
In the LocationsViewModel, send the LocationModel object you wanted to use in the LocationViewModel constructor by just sending it.
public void ExecuteAddCommand()
{
MessengerInstance.Send<LocationModel>(LocationModelObj);
_navigationService.navigate(tyepof(LocationPage));
}
For this to succeed though, you'd need to register LocationViewModel to register to receive LocationModel object before sending the object from LocationsViewModel. So, you need to create your view model immediately by using an overload of SimpleIoc's Register method.
SimpleIoc.Default.Register<LocationViewModel>(true);
Based on what #Shridhar said The SimpleIoc container couldn't create the view model instance as the constructor requires a LocationModel object which you can't pass directly, I thought I'd try adding a parameterless constructor but I got another error i.e.
Cannot register: Multiple constructors found in LocationViewModel but
none marked with PreferredConstructor.
So I marked my parameterless constructor with the PreferredConstructor as such:
[PreferredConstructor]
public LocationViewModel()
{
}
This sorted my problem but as mentioned to #Shridar, I'm not sure whether or not this is the correct solution so I will spend more time investigating and see if this works as expected and doesn't have any side effects.
I'll update as soon as I have something.
I also experienced a similar error while trying to use MVVMLight DialogService; the solution was to make sure it is registered in the ViewModelLocator
public ViewModelLocator()
{
SimpleIoc.Default.Register<IDialogService, DialogService>();
}

How do I conditionally set the destination object to null using automapper

I am trying to do something like this:
AutoMapper.Mapper.CreateMap<UrlPickerState, Link>()
.ForMember(m=>m.OpenInNewWindow,map=>map.MapFrom(s=>s.NewWindow))
.AfterMap((picker, link) => link = !string.IsNullOrWhiteSpace(link.Url)?link:null) ;
var pickerState = new UrlPickerState();
var linkOutput = AutoMapper.Mapper.Map<Link>(pickerState);
However, the assigned value of link is not used in any execution path.
I would like linkOutput to be null, but it is not.
How would I make the destination object null?
Details of objects involved:
public class Link
{
public string Title { get; set; }
public string Url { get; set; }
public bool OpenInNewWindow { get; set; }
}
public class UrlPickerState
{
public string Title { get; set; }
public string Url { get; set; }
public bool NewWindow { get; set; }
//.... etc
}
Here's a fiddle: http://dotnetfiddle.net/hy2nIa
This is the solution I used in the end, it was a bit more manual internally, but does not require any extra plumbing.
If anyone has a more elegant solution, it would be appreciated.
config.CreateMap<UrlPickerState, Link>()
.ConvertUsing(arg =>
{
if (string.IsNullOrWhiteSpace(arg.Url))
{
return null;
}
return new Link()
{
Url = arg.Url,
OpenInNewWindow = arg.NewWindow,
Title = arg.Title,
};
});
I created the following extension method to solve this problem.
public static IMappingExpression<TSource, TDestination> PreCondition<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mapping
, Func<TSource, bool> condition
)
where TDestination : new()
{
// This will configure the mapping to return null if the source object condition fails
mapping.ConstructUsing(
src => condition(src)
? new TDestination()
: default(TDestination)
);
// This will configure the mapping to ignore all member mappings to the null destination object
mapping.ForAllMembers(opt => opt.PreCondition(condition));
return mapping;
}
For the case in question, it can be used like this:
Mapper.CreateMap<UrlPickerState, Link>()
.ForMember(dest => dest.OpenInNewWindow, opt => opt.MapFrom(src => src.NewWindow))
.PreCondition(src => !string.IsNullOrWhiteSpace(src.Url));
Now, if the condition fails, the mapper will return null; otherwise, it will return the mapped object.
I think that will have to be done outside the mapping. Since AutoMapper requires an instance to map to, setting the destination to null seems like it should go outside the mapping.
I would instead do something like:
AutoMapper.Mapper.CreateMap<UrlPickerState, Link>()
.ForMember(m=>m.OpenInNewWindow,map=>map.MapFrom(s=>s.NewWindow));
var pickerState = new UrlPickerState();
Link linkOutput = null;
if(!string.IsNullOrWhiteSpace(pickerState.Url)) // or whatever condition is appropriate
linkOutput = AutoMapper.Mapper.Map<Link>(pickerState);

How can I reuse a DropDownList in several views with .NET MVC

Several views from my project have the same dropdownlist...
So, in the ViewModel from that view I have :
public IEnumerable<SelectListItem> FooDdl { get; set; }
And in the controller I have :
var MyVM = new MyVM() {
FooDdl = fooRepository.GetAll().ToSelectList(x => x.Id, x => x.Name)
}
So far so good... But I´m doing the same code in every view/controller that have that ddl...
Is that the best way to do that?
Thanks
I'd say that's fine to be honest, as it's only a repeat of a few lines of code. If it's really bothering you though, you could have all your controllers inherit from a BaseController (if they don't already) and store a method in there to get them all, something like:
public IEnumerable<SelectListItem> GetFoos()
{
return fooRepository.GetAll().ToSelectList(x => x.Id, x => x.Name);
}
Then in your controllers you could do:
var MyVM = new MyVM() {
FooDdl = GetFoos()
}
If your DropDownList is exactly the same the approach I would use is:
1) In your Base Controller or in a Helper class, you can create a method that returns a SelectList. That method should receive a nullabe int to get the select list with a value pre selected.
2) It is wise to cache the information you list in the DDL, to not query the database too often.
So, for (1):
public SelectList GetMyDDLData(int? selectedValue){
var data = fooRepository.GetAll().Select(x => new { Value = x.Id, Text = x.Name });
return new SelectList(data, "Id","Name", selectedValue);
}
In the view model:
var myVM = new MyVM();
myVM.DDLData = this.GetMyDDLData(null) // if it is in your BaseController.
myVM.DDLData = YourHelperClass.GetMyDDLData(null) // if it is in a helper static class
In your views:
#Html.DropDownListFor(x => x.FooProp, Model.DDLData, "Select one...")
For number (2):
private IEnumerable<YourModel> GetMyData()
{
var dataItems = HttpContext.Cache["someKey"] as IEnumerable<YourModel>;
if (dataItems == null)
{
// nothing in the cache => we perform some expensive query to fetch the result
dataItems = fooRepository.GetAll().Select(x => new YourModel(){ Value = x.Id, Text = x.Name };
// and we cache it so that the next time we don't need to perform the query
HttpContext.Cache["someKey"] = dataItems ;
}
return dataItems;
}
The "someKey" could be something specific and static is this data is the same to all users, or you can do "someKey" + User.Id if the data is specific to one user.
If your repository is an abstractin layer (not directly EntityFramework) you can place this code there.
We also use a static class :
public static class SelectLists
{
public static IList<SelectListItem> CompanyClasses(int? selected)
{
var session = DependencyResolver.Current.GetService<ISession>();
var list = new List<SelectListItem>
{
new SelectListItem
{
Selected = !selected.HasValue,
Text = String.Empty
}
};
list.AddRange(session.All<CompanyClass>()
.ToList()
.OrderBy(x => x.GetNameForCurrentCulture())
.Select(x => new SelectListItem
{
Selected = x.Id == (selected.HasValue ? selected.Value : -1),
Text = x.GetNameForCurrentCulture(),
Value = x.Id.ToString()
})
.ToList());
return list;
}
}
In the view we have nothing special :
#Html.DropDownListFor(x => x, SelectLists.CompanyClasses(Model))
And sometime we also create an EditorTemplate so it's faster to reuse like this
Model :
[Required, UIHint("CompanyClassPicker")]
public int? ClassId { get; set; }
EditorTemplate :
#model int?
#if (ViewBag.ReadOnly != null && ViewBag.ReadOnly)
{
var item = SelectLists.CompanyClasses(Model).FirstOrDefault(x => x.Selected);
if (item != null)
{
<span>#item.Text</span>
}
}
else
{
#Html.DropDownListFor(x => x, SelectLists.CompanyClasses(Model))
}
Create object with getter for your dropdown values:
public static class DropDowns
{
public static List<SelectListItem> Items {
get
{
//Return values
}
}
}
Create Razor partial:
#Html.DropDownListFor(m => "ChoosenItem", DropDowns.Items, "")
Call partial:
#Html.RenderPartial("DropDownItems")
And finally receive ChoosenItem value in controller. Simply.
I use an IModelEnricher combined with Automapper and attributes that define relationships between a type of list and select list provider. I return an entity etc using a specific ActionResult that then automaps my entity to a ViewModel and enriches with data required for select lists (and any additional data required). Also keeping the select list data as part of your ViewModel keeps your controller, model, and view responsibilities clear.
Defining a ViewModel ernicher means that anywhere that ViewModel is used it can use the same enricher to get its properties. So you can return the ViewModel in multiple places and it will just get populated with the correct data.
In my case this looks something like this in the controller:
public virtual ActionResult Edit(int id)
{
return AutoMappedEnrichedView<PersonEditModel>(_personRepository.Find(id));
}
[HttpPost]
public virtual ActionResult Edit(PersonEditModel person)
{
if (ModelState.IsValid){
//This is simplified (probably don't use Automapper to go VM-->Entity)
var insertPerson = Mapper.Map<PersonEditModel , Person>(person);
_personRepository.InsertOrUpdate(insertPerson);
_requirementRepository.Save();
return RedirectToAction(Actions.Index());
}
return EnrichedView(person);
}
This sort of ViewModel:
public class PersonEditModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public int FavouriteTeam { get; set; }
public IEnumerable<SelectListItem> Teams {get;set;}
}
With this sort of Enricher:
public class PersonEditModelEnricher :
IModelEnricher<PersonEditModel>
{
private readonly ISelectListService _selectListService;
public PersonEditModelEnricher(ISelectListService selectListService)
{
_selectListService = selectListService;
}
public PersonEditModelEnrich(PersonEditModel model)
{
model.Teams = new SelectList(_selectListService.AllTeams(), "Value", "Text")
return model;
}
}
One other option is to decorate the ViewModel with attributes that define how the data is located to populate the select list. Like:
public class PersonEditModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public int FavouriteTeam { get; set; }
[LoadSelectListData("Teams")]
public IEnumerable<SelectListItem> Teams {get;set;}
}
Now you can decorate an appropriate method in your select service with an attribute like:
[ProvideSelectData("Teams")]
public IEnumerable Teams()
{
return _teamRepository.All.ToSelectList(a => a.Name, a => a.TeamId);
}
Then for simple models with no complex enrichment just the generic enrichment process can handle it. If you want to do anything more complex you can define an enricher and it will be used if it exists.
Other options could be a convention over configuration approach where the Enricher looks at property name and type e.g. IEnumerable<SelectListItem> PossibleFirstDivisionTeams {get;set;} then matches this if it exists with a select list provider name in a class that say implements a marker interface ISelectListProvider. We went the attribute based one and just created Enums representing the various lists E.g. SelectList.AllFirstDivisionTeams. Could also try interfaces on ViewModel that just have a property collection for a selectlist. I don't really like interfaces on my ViewModels so we never did this
It all really depends on the scale of your application and how frequently same type of select list data is required across multiple models. Any specific questions or points you need clarified let me know
See this question. Also this blog post and this. Also this question on Automapper forum
The first question is if the options-list belongs to the ViewModel. A year or two ago I did the same, but what I see recently more and more as a "best practice" is that people add the list to the ViewBag/ViewData not to the ViewModel. That's an option and I tend to do the same for a one-shot drop-down list, but it doesn't answer the code-reuse question you are facing. For that I see two different approaches (and two more that I rule out).
Shared editor template. Create an editor template for the type that's represented by the dropdown. In this case - because we don't have the list of possible options in the ViewModel or the ViewBag - the template has to reach out for the options to the server. That's possible by adding an action method (that returns json) to a controller class. Either to a shared "LookupsController" (possibly an ApiController) or to the controller that the list-items' type belongs to.
Partial view. The drop down values belong to some type. The Controller of that type could have an action method that returns a partial view.
The benefit of the first one is that a nice #Html.EditorFor call will do the job. But I don't like the ajax dependency. Partly for that reason I would prefer the partial view.
And there is a third one: child action, but I don't see that a good pattern here. You can google what's the difference between child actions and partial views, for this case child action is the wrong choice. I also wouldn't recommend helper methods. I believe they are not designed for this use case either.
You could put that fetch in the default (null) constructor of MyVM if you don't need to vary its content.
Or you could use a PartialView that you render into the views that need t.
I like to use static classes often in a helper class that I can call from any view.
#Html.DropDownListFor(x => x.Field, PathToController.GetDropDown())
and then in your controller have a method built like this
public static List<SelectListItem> GetDropDown()
{
List<SelectListItem> ls = new List<SelectListItem>();
lm = (call database);
foreach (var temp in lm)
{
ls.Add(new SelectListItem() { Text = temp.name, Value = temp.id });
}
return ls;
}
Hopefully it helps.
If you really don't want to duplicate the code, place the code from the controllers into a helper class, and render the dropdown within a shared view (like _Layout.cshtml) that you'd then have to implement into your views by RenderPartial.
Create a partial view, _MyDropdownView.cstml, which uses the helper class you threw the code from the controllers in with something like the following:
#using MyNamespace.MyHelperClass
<div id="myDropdown">#Html.DropDownListFor(model => model.Prop, MyVM as SelectList, "--Select a Property--")</div>
Then, within your views:
#Html.RenderPartial("_MyDropdownView")
Extension methods to the rescue
public interface ISelectFoo {
IEnumerable<SelectListItem> FooDdl { get; set; }
}
public class FooModel:ISelectFoo { /* implementation */ }
public static void PopulateFoo(this ISelectFoo data, FooRepository repo)
{
data.FooDdl = repo.GetAll().ToSelectList(x => x.Id, x => x.Name);
}
//controller
var model=new ViewModel();
model.PopulateFoo(repo);
//a wild idea
public static T CreateModel<T>(this FooRepository repo) where T:ISelectFoo,new()
{
var model=new T();
model.FooDdl=repo.GetAll().ToSelectList(x => x.Id, x => x.Name);
return model;
}
//controller
var model=fooRepository.Create<MyFooModel>();
What about a Prepare method in a BaseController?
public class BaseController : Controller
{
/// <summary>
/// Prepares a new MyVM by filling the common properties.
/// </summary>
/// <returns>A MyVM.</returns>
protected MyVM PrepareViewModel()
{
return new MyVM()
{
FooDll = GetFooSelectList();
}
}
/// <summary>
/// Prepares the specified MyVM by filling the common properties.
/// </summary>
/// <param name="myVm">The MyVM.</param>
/// <returns>A MyVM.</returns>
protected MyVM PrepareViewModel(MyVM myVm)
{
myVm.FooDll = GetFooSelectList();
return myVm;
}
/// <summary>
/// Fetches the foos from the database and creates a SelectList.
/// </summary>
/// <returns>A collection of SelectListItems.</returns>
private IEnumerable<SelectListItem> GetFooSelectList()
{
return fooRepository.GetAll().ToSelectList(foo => foo.Id, foo => x.Name);
}
}
You can use this methods in the controller:
public class HomeController : BaseController
{
public ActionResult ActionX()
{
// Creates a new MyVM.
MyVM myVm = PrepareViewModel();
}
public ActionResult ActionY()
{
// Update an existing MyVM object.
var myVm = new MyVM
{
Property1 = "Value 1",
Property2 = DateTime.Now
};
PrepareViewModel(myVm);
}
}
Have an interface with all your properties that need to be automatically populated:
public interface ISelectFields
{
public IEnumerable<SelectListItem> FooDdl { get; set; }
}
Now all your view models that want to have those properties, implement that interface:
public class MyVM : ISelectFields
{
public IEnumerable<SelectListItem> FooDdl { get; set; }
}
Have a BaseController, override OnResultExecuting, find the ViewModel that is passed in and inject the properties to the interface:
public class BaseController : Controller
{
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
var viewModel = viewResult.Model as ISelectFields;
if (viewModel != null)
{
viewModel.FooDdl = fooRepository.GetAll().ToSelectList(x => x.Id, x => x.Name)
}
}
base.OnResultExecuting(filterContext);
}
}
Now your controllers are very simple, everything is strongly typed, you are sticking with the DRY principle and you can just forget about populating that property, it will always be available in your views as long as your controllers inherit from the BaseController and your ViewModels implement the interface.
public class HomeController : BaseController
{
public ActionResult Index()
{
MyVM vm = new MyVM();
return View(vm); //you will have FooDdl available in your views
}
}
Why not use the advantages of RenderAction:
#(Html.RenderAction("ControllerForCommonElements", "CommonDdl"))
Create a controller, and an action that returns the Ddl and and just reference it in the views.
See some tips here on how you could use it
This way you can also cache this result. Actually the guys building StackOverflow talked about the pros of using this combined with different caching rules for different elements (i.e. if the ddl does not need to be 100% up to date you could cache it for a minute or so) in a podcast a while ago.

Categories