ASP.NET MVC navigation route that changes based on viewed data - c#

I am in the process of creating tabbed navigation where the route location can vary. The parameter used to render the tabs should be based on the presently viewed data (which when it is a user, may not be the logged in user).
In the example image this is a user. Therefore, if I am looking at Andrew Steele then the links should be contextual to Andrew Steele (Andrew's summary, computers, accounts etc.). When I am looking at Bruce Hamilton the links should be contextual to Bruce Hamilton (Bruce's summary, computers, accounts etc.).
I've solved this by sticking the necessary parameter value in each ViewModel and then passing the value onto a partial to render the links; this feels kludgey. I'd prefer to not shove the linking parameter data into each ViewModel. It would seem reasonable to use Html.Action or Html.RenderAction combined with a ViewData Dictionary value, but I tend to shy away from using "magic strings" where possible.
Is there a better way to get the parameter value to the view that I am missing?

I wrote a site a while back where I had different tabs that went to different places for different users. I'll walk you through my solution hopefully I understood the question correctly and some of this helps.
As far as getting data to and from the View, I do use the ViewDataDictionary. To the best of my knowledge, that's what it's for when your model doesn't consist of a single simple object. In order to get around the "magic strings" of view keys, I create a bunch of extension methods on the ViewDataDictionary. This has the drawback that you end up with a slew of extra methods, but at least all of your string keys are isolated in a single location. You could even go the extra step of create constants in the class, but it seems redundant when only this class uses them. Extension properties would be better but...
/// <summary>
/// Gets the list of tabs to show.
/// </summary>
/// <param name="dictionary"></param>
/// <returns></returns>
public static IList<TabItemDisplay> TabListGet(this ViewDataDictionary dictionary)
{
IList<TabItemDisplay> result;
if (dictionary.ContainsKey("TabList"))
result = dictionary["TabList"] as IList<TabItemDisplay>;
else
result = null;
return result;
}
/// <summary>
/// Sets the list of tabs to show.
/// </summary>
/// <param name="dictionary"></param>
/// <param name="tabList"></param>
/// <returns></returns>
public static IList<TabItemDisplay> TabListSet(this ViewDataDictionary dictionary, IList<TabItemDisplay> tabList)
{
dictionary["TabList"] = tabList;
return tabList;
}
You'll notice that I have an explicit view object, TabItemDisplay, that I pass into the dictionary. This contains all of the values necessary to pass to Html.ActionLink.
public class TabItemDisplay
{
public string Name { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public object RouteValues { get; set; }
}
Since this view is not the main content of the page, I prefer to put the logic of creating the tab items, including destination parameters, into an ActionFilter. This allows me to reuse the tab creation logic across different actions and controllers. Any View that contains the tab partial control gets the CreatTabAttribute slapped across the corresponding Action or Controller and it's good to go.
This may be more than you needed, but I hope some of it helps.
EDIT: Just realized I didn't include what this looks like in the partial view. I actually have an HtmlHelper extension that renders a more intricate tab, but you get the idea.
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<div id="tabs">
<%
if (null != ViewData.TabListGet()) {
foreach(var item in ViewData.TabListGet()) {
%>
<%= Html.ActionLink(item.Name, item.Action, item.Controller, item.RouteValues, null)%>
<%
}
}
%>
</div>
EDIT: Adding a short example of the ActionFilter I use.
public class CreateContentTabsAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var result = filterContext.Result as ViewResultBase;
if (null == result) return;
var routeValues = filterContext.RouteData.Values;
var repository = ObjectFactory.GetInstance<ITabRepository>();
var context = filterContext.HttpContext;
var userName = context.User.Identity.Name; // Or get id from Membership.
var tabs = repository.ReadByUserId(userName);
TabItemDisplay defaultTab = null;
var tabItems = new List<TabItemDisplay>();
foreach (var tab in tabs)
{
var tabItem = new TabItemDisplay
{
Name = tab.Name,
Action = "View",
Controller = "Tab",
RouteValues = new { key = tab.Key }
};
tabItems.Add(tabItem);
}
if (context.Request.IsAuthenticated)
{
tabItems.Add(new TabItemDisplay
{
Name = "Account",
Action = "ChangePassword",
Controller = "Account",
RouteValues = new { siteKey = site.Key }
});
}
result.ViewData.TabListSet(tabItems);
}
}
This is only a basic example of pulling tabs from a repository (instantiated using StructureMap), and a simple check to see if the user is authenticated. But you can do other things such as pull the requested user id for the user being displayed from routeValues.

You can create a viewmodel for this that exposes the tab preference. If each tab is totally different you could have a viewmodel base class that exposes the tab preference and each of the tabs could have their own view model.

Personally, I'd use either RenderAction from the MVC futures or a render partial call here depending on performance requirements and taste.
Advantage to RenderAction is you won't need to pass all the required rendering data to the delegate controller action as it could do the lookup itself. Whereas a partial view would require your main page have enough view data to render the tab. Either way its just two for loops (one for the tab list, one for the tab rendering) and a bit of extra data in your Model.

one solution is to create base controller and have all ur controllers inherit from it. in base controller u can add necessary value into viewmodel
public applicationController:Controller
{
ViewData["linkvals"] = someValues;
}
public HomeContorller:ApplicationController{}
second solution is to create a base view model and have all ur viewmodels inherit it. in constructor of base viewmodel u can create link values and assign them to ur properties
public baseViewModel
{
public LinkVal{get;set;}
public baseViewModel()
{
//calculate link vals
}
}
public ViewModelA:baseViewModel{}

Related

Asp.Net Core MVC 7 C# - Recommended approach of passing data to and from controller from a page that has multiple forms and states

I'm building a simple web portal using the .Net Core 7 MVC template in VS2022 (It's challenging to know if this is the latest recommended format and or template, Microsoft doesn't really state what is the current approach from a stack perspective).
As part of building out and focusing on MVC I looked at if PageModels were the recommended method of binding to the page or if using a simple Model and populating the page using razor syntax (Which is my preference) was the most modern recommendation (Again Microsoft documentation isn't exactly specific).
That aside, the specific problem that I have is that I'm building out a settings page that has a number of different forms and I initially thought that in order to maintain the page state of other inputs and fields that when a form posts to the controller it sends the entire page model with the form bound properties populated, perform some logic, and pass a modified model back the the main Index ActionMethod. However I can't seem to see anything specific around sending the entire model back that was used by the razor page as part of individual form submits. Or moreover, if it's even a good idea.
The page structure looks like this.
Form - Team Name
Button - Submit, just saves the team name.
List - Team Members currently in team.
Form - Search for Users by Email.
List - List of Users matching Search input, with button to Add To Team
The issue i have is that if I pass back the form properties which are bound to the model from the search input, i don't get all of the other model properties used for populating current team members list, or the team name, so I assume that would mean that the controller has to do the work of getting that data again? This seems a bit inefficient in my mind. I'm assuming there is a clever way of doing this and achieving an appropriate outcome but I'm struggling to see the light through the dark with this particular consideration. Any suggestions or nods in the right direction are appreciated.
Example Model
public class SettingsPageModel
{
public SettingsPageModel() { }
public SettingsPageModel(string teamName)
{
TeamName = teamName;
}
public SettingsPageModel(SearchModel searchModel)
{
SearchModel = searchModel;
}
public SettingsPageModel(string teamName, List<TeamMember> teamMembers, SearchModel searchModel)
{
TeamName = teamName ?? throw new ArgumentNullException(nameof(teamName));
TeamMembers = teamMembers ?? throw new ArgumentNullException(nameof(teamMembers));
SearchModel = searchModel ?? throw new ArgumentNullException(nameof(searchModel));
}
public string TeamName { get; set; } = string.Empty;
public List<TeamMember> TeamMembers { get; set; } = new();
public SearchModel SearchModel { get; set; } = new();
public DialogModel? DialogModel { get; set; }
}
public class SearchModel
{
public string? SearchTerm { get; set; }
public List<User>? SearchedUsers { get; set; }
}
Every time you submit a form you send back ONLY the input elements on that form (not the entire model).
If you have a search input and want to update only a portion of the page you would have to either:
Add more hidden input fields to that form to send for example TeamName and populate the model completely (not ideal)
Use Ajax when the form is submitted, have an Action in your controller that gets the filtered users and return a PartialView() with the list of users, and update the DOM using JavaScript when you get your ajax response (HTML)

Asp.net MVC Composite class custom model binding

I've searched all over the web but not finding the solution for the following problem:
Say I have three ViewModel classes
public class ViewModelNewPerson
{
public string PersonName;
public string Address;
public string EyeColor;
//etc
}
public class ViewModelSelectPerson
{
public int SelectedPersonId;
}
public class ViewModelComposite
{
public ViewModelSelectPerson selectViewModel;
public ViewModelNewPerson newPersonViewModel;
}
and I want to do the following things:
In the Controller I want to create a GET Action which uses the class ViewModelComposite as its Get model, and in the view I want the user choose from the following two available actions: to choose a existed person, and to add a new person as the selected value.
So I need to create two forms in the View, and there would be two POST Actions added to the Controller using the Post model of class ViewModelNewPerson and ViewModelSelectPerson.
My question is, how can I do the manual model binding using a Custom Model Binder that can convert the Composite class of ViewModelComposite to ViewModelNewPerson in the Action of create a new person, and to ViewModelSelectPerson in the Action of select an existing person?
EDIT:
Now I have an idea of decomposing the class ViewModelComposite and declare every property in the two classes into the composite class, and the default model binder will do the trick, I think. But that'll drop the composite pattern, and is not something I wanted.
You would use one single view model in your form, you would have a post action that receives your single view model.
In your Controller code:
public ActionResult GetSomeData(MyCustomViewModel model){
// add the first element
var person = Person.Add(model.Person);
// update the second object in model, with related / needed ID
model.PersonContent.PersonId = person.id;
// add in related content
var AddedContent = PersonContent.Add(model.PersonContent);
}
single form, multiple actions, multiple tables

Is there any way to remove a view (by name) from a Prism region when the view was added using the RegionManager.RequestNavigate method?

I am using Prism for navigation in my WPF MVVM application. I register my view as follows.
// MyView is the data type of the view I want to register and "MyView"
// is the name by which I want the data type to be identified within
// the IoC container.
_container.RegisterType<object, MyView>("MyView");
I display this view as follows.
_regionManager.RequestNavigate(
"MyRegion", // This is the name of the Region where the view should be displayed.
"MyView" // This is the registered name of the view in the IoC container.
);
Elsewhere in the application, I need to remove this view in an event handler; however, the following code returns an ArgumentNullException.
_regionManager.Regions["MyRegion"].Remove(
_regionManager.Regions["MyRegion"].GetView("MyView")
);
This indicates that the RequestNavigate method does not add MyView to MyRegion using the name "MyView". I know that if I were to use the _regionManager.Add(MyView, "MyView") method, the GetView method would not return null. Unfortunately, RequestNavigate does not seem to handle the view name in the same way. Is there any way to remove a view (by name) from a region when the view was added using the RequestNavigate method?
It stems from how you add your view, not with your removal. Previously asked answered by adding the view fully, aka including the name.
_regionManager.Regions["MyRegion"].Add(myView, "MyView");
So now you can do your retrieval and removal:
var theView = _regionManager.Regions["MyRegion"].GetView("MyView");
_regionManager.Regions["MyRegion"].Remove(theView);
Without defining name during Regions.Add()
In your View, define a property that is accessible (public if multi-project, internal if all in one project). Use this property in everything, one example would be a public string ViewTitle { get { return "XYZ"; } }. Then retrieve from the Views the item that has the desired ViewTitle. The Views collection is the collection of views in that region, so you can use dynamic in .NET 4.0+ to ignore the type and get the property/function you specify, assuming it is there. Another option is to make your imported ViewModel in the View have a getter rather than just setting the DataContext, then you'd check the property "is" to the ViewModel you're looking for. Removes the string search but exposes the view's datacontext. So probably make an enum like I would do with the region.
I included everything in my View's .cs file so you can see how it works without complicating it or really breaking MVVM.
[ViewSortHint("050")]
[ViewExport(RegionName = RegionNames.WorkspaceTabRegion)]
[PartCreationPolicy(CreationPolicy.Shared)]
public partial class AView : UserControl
{
public AView()
{
InitializeComponent();
}
[Import]
[SuppressMessage("Microsoft.Design", "CA1044:PropertiesShouldNotBeWriteOnly", Justification = "MEF requires property; never retrieved")]
PrintingViewModel ViewModel { set { this.DataContext = value; } }
public string ViewTitle { get { return "AView"; } }
}
Now in the ViewModel at some point:
var viewToRemove = RegionManager.Regions[RegionNames.WorkspaceTabRegion].Views.FirstOrDefault<dynamic>(v => v.ViewTitle == "AView");
RegionManager.Regions[RegionNames.WorkspaceTabRegion].Remove(viewToRemove);
We recently found ourselves with the same problem; thanks #odysseus.section9 for pointing its root in your comment, it really helped.
We considered making all views implement an interface having a Name property but didn't feel quite right. Then we explored #bland solution but felt uncomfortable about using dynamic so we went for a very similar approach using reflection.
Since we are also already using the ViewExportAttribute to export our views and it contains the desired ViewName property, what we do is querying for each view in a region for its attributes, looking for the ViewExportAttribute and checking the value of the ViewName property. Although in our design all views are annotated with it, the query tolerates views that don't - it simply ignores them.
For convenience we created an extension method for IRegion which searches for the views with the desired name within a region. Also, we added two extension methods to IRegionManager for two common scenarios in our application: re-using an existing view or navigating and removing all existing views (matching a name) and navigating. I think the latter solves your need just by getting rid of the call to
public static IEnumerable<object> FindViews(this IRegion region, string viewName)
{
return from view in region.Views
from attr in Attribute.GetCustomAttributes(view.GetType())
where attr is ViewExportAttribute && ((ViewExportAttribute)attr).ViewName == viewName
select view;
}
public static void ActivateOrRequestNavigate(this IRegionManager regionManager, string regionName, string viewName, UriQuery navigationParams)
{
IRegion region = regionManager.Regions[regionName];
object view = region.FindViews(viewName).FirstOrDefault();
if (view != null)
region.Activate(view);
else
regionManager.RequestNavigate(regionName,
new System.Uri(navigationParams != null ? viewName + navigationParams.ToString() : viewName, UriKind.Relative));
}
public static void RemoveAndRequestNavigate(this IRegionManager regionManager, string regionName, string viewName, UriQuery navigationParams)
{
IRegion region = regionManager.Regions[regionName];
foreach (object view in region.FindViews(viewName))
region.Remove(view);
regionManager.RequestNavigate(regionName,
new System.Uri(navigationParams != null ? viewName + navigationParams.ToString() : viewName, UriKind.Relative));
}

Placement of a Function in an ASP.NET MVC Application

For educational purposes, I am building a clone social bookmarking service (similar to reddit). Under each 'bookmark' in the list, I'd like to show the base domain of the source URL (as opposed to the full URL.
I've already found a few threads on SO on how to do this, so I've gone ahead and made a class to abstract the functionality, but I'm unsure of where I should actually be calling the method.
At the moment I have my BookmarkList controller method passing a list of Bookmark objects to the view, where I'm iterating over the list. Since the Bookmark object doesn't have a property for storing the base URL (as I'm computing it on the fly) I can't put it inside the Bookmark objects before passing them to the view (and it seems wrong anyway). So should I be calling the GetDomainFromUrl method I've made from the view itself? For some reason that doesn't feel appropriate either.
I am unsure of how to fit in this functionality without breaking MVC convention.
Thanks.
I would add it to the Bookmark class. Properties can be computed, here is an example from a tutorial on asp.net mvc from msdn (MSDN source):
public string LastName { get; set; }
public string FirstMidName { get; set; }
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
Instead of simply concatinating strings, you would call your GetDomainFromUrl method here.
IE,
public string BaseUrl
{
get
{
return GetDomainFromUrl(this.Url);
}
}
Notice that there is not a set method defined, since you could/would be setting the Url property.
Also, is your Url stored as a string?
If you are using the URI class, you could just use this.FullUrl.Host, assuming your article's url is defined in a property called FullUrl.(also assuming that this would not defeat the point of this assignment, since you said this was for school.)
For very basic scenarios, or where you have full control over your models, MVC (Model, View, Controller) is a good pattern.
In my experience, you typically need additional information that is important to your views but not to your actual model. For example, a list of dropdown items to be displayed for a model property, or in your case, putting the base URL for a site for your users to see.
In this case, I like to adapt MVC to be VM-V-C (ViewModel, View, Controller).
Essentially, you would want to create a Bookmark ViewModel and use that when rendering your views:
BookmarkViewModel.cs:
public class BookmarkViewModel
{
public string BaseUrl {get;set;}
// + all existing bookmark properties
}
You can either add your base URL function right into your view model and have the view model perform the function itself, or you can do it in your controller when creating the view model.
There are a few different options on how you could do this. I recommend storing the GetDomainFromUrl() method in the Bookmark Class. Also I recommend creating a property for the BaseUrl as well.
You can then either pass the full Url to the Bookmark object in the constructor, perform your function, and set it to the BaseUrl property.
class Bookmark
{
public string BaseUrl { get; }
public Bookmark(string url)
{
BaseUrl = GetDomainFromUrl(url);
}
private string GetDomainFromUrl(string url)
{
//your logic to generate BaseUrl
}
}
Another alternative is to do something like the following:
class Bookmark
{
private string baseUrl;
public string BaseUrl
{
get
{
return baseUrl;
}
set
{
baseUrl = GetDomainFromUrl(value));
}
}
private string GetDomainFromUrl(string url)
{
//your logic to generate BaseUrl
}
}
and then set the BaseUrl property somplace in your code to the value of the full url, and when you do so it will perform your function and store it in your property.

Passing and returing data to a simple grid form using ASP.NET MVC

I have page with a simple table and advanced search form. I pass List<Customers> to the model:
View(List<Customers>);
So what is best way to pass and return data to the search form? I want to use validation or something but I think passing data through ViewData is not good idea. Any suggestions?
You should wrap all your data that is required by you view in a model specific to that view. The advantage to this is you could also include your search criteria in the model which would be empty at first but when your search posted, the model would automatically contain your search criteria so you could reload it when passing back the results. This will help maintain your state between post's as well.
This also allows all your view's data to be type safe where ViewData would not be.
Eg:
public class CustomerSearchViewModel
{
public List<Customer> Customers { get; set; }
// your search criteria if you want to include it
public string SearchFirstName { get; set; }
public string SearchLastName { get; set; }
public int SearchCustomerID { get; set; }
// etc...
}
When you return back the List<Customer> the search criteria would already be filled in your model from the post so your view can default the search criteria back to the corresponding controls (assuming your search results and search inputs controls are on the same view).
For example, in your post you would accept a CustomerSearchViewModel. Then all you need to do is get your list of customers and add it back to the model and return the same model.
// assuming you have accepted a CustomerSearchViewModel named model
model.Customers = GetCustomersForSearchCriteria(model.SearchFirstName,
model.SearchLastName, model.SearchCustomerID);
return View(model);
You could also add the validation attributes to your model properties to leverage the built in validation in MVC. This would not be possible if you were using ViewData to pass this data around.
You have to also consider the 'next guy'. It's cleaner when all the data that the view requires is located in a single class. This way they don't have to hunt through the code to discover if ViewData is being used and what data is actually being passed around in it.
ViewData is still an option for passing data but I try to minimize the use of it if at all possible.
Rather than passing just a list of items to your View, create a class which contains your list of items and any other data you might need, i.e. a ViewModel.
public class CustomerSearchViewModel {
public IEnumerable<Customer> Customers { get; set; }
public string SearchTerm { get; set; }
}
.....
var viewModel = new CustomerSearchViewModel {
Customers = customerList,
SearchTerm = searchTerm
};
return View(viewModel);

Categories