This is a little bit abstract:
I have a website written in ASP.NET MVC. The end user can create a Menu object that will be displaced in the menu of the site. This looks something like this:
public class Menu
{
public string Caption { get; set; }
public string Address { get; set; }
public Menu(string Caption, string Address)
{
this.Caption = Caption;
this.Address = Address;
}
}
public Menu Menu1 = new("Home", "/");
public Menu Menu2 = new("Events", "/events");
public Menu Menu3 = new("Specific Event", "/event/13");
When a request comes in, I need to be able to determine which menu item is "Active". In addition, if a request is for a "subpage" of an item which is not explicitly named, the menu item should also be marked as active (e.g. if the request is for "/event/95/checkout", then Menu2 would be marked as active).
I was thinking about trying to figure compare the relative URL with the Address of the URLs to determine which matches the most characters, starting at the beginning of the string - but how would I go about doing that? Is there perhaps a better way?
We have a similar thing in our code. Our master page has a menu on the left, but one menu item can correlate to a range of sub pages.
The solution we went with was for each menu item to hold a list of regular expressions.
I'll give you a bunch of snippets of our code. This isn't a self-contained solution, but may give you ideas...
We would define our menu using a builder pattern (notice the method UrlMatchPattern)
var builder = new SiteMapBuilder();
builder.Add
.Title("Home")
.LaunchUrl("~/Home/Index")
.IconUrl("~/Images/menuIcons/home.png")
.UrlMatchPattern("~/Home/Index/?.*");
builder.Add
.Title("Network")
.IconUrl("~/Images/menuIcons/network.png")
.Children(b =>
{
b.Add.Title("Companies")
.LaunchUrl("~/Company")
.UrlMatchPattern("~/Company");
b.Add.Title("Groups")
.LaunchUrl("~/PlayerGroup")
.UrlMatchPattern("~/PlayerGroup")
.LimitToRoles(
CmsUserRoleId.Administrators,
CmsUserRoleId.Support
);
b.Add.Title("Players")
.LaunchUrl("~/Player/index")
.UrlMatchPattern(
#"~/\bPlayer\b",
"~/Player/Index",
#"~/Admin/PlayerDiagnostics\.aspx"
);
This builder would dynamically rebuild the menu on each page request, generating a bunch of navigation panel nodes:
internal NavPanelNode GenerateNode(IWebContext webContext, ...)
{
return new NavPanelNode
{
Title = _title,
Url = _url,
Children = _childNodeBuilder == null ? Enumerable.Empty<NavPanelNode>() : _childNodeBuilder.GenerateNodeHierachy(webContext, userContext),
IsSelected = _urlMatchPattern.Any(pattern => webContext.MatchesPath(pattern)),
IconUrl = _iconUrl
};
}
And my MatchesPath method:
public static bool MatchesPath(this IWebContext webContext, System.Text.RegularExpressions.Regex matchPattern)
{
string appRelativePath = webContext.CurrentRequestContext.GetAppRelativePath();
return matchPattern.IsMatch(appRelativePath);
}
As mentioned, our matching logic is via regular expressions.
Of course, if you're making this end-user configurable, you will have to let them configure the matching logic. I don't know how you will do this - it depends upon the user interface you're creating.
Related
I am new to .net core - have been using aspx web pages and .net framework 4.x for a number of years. I have a project where we want to display different controls (textbox, dropdown, checkbox) on the page based on values returned from a query. For example, user chooses "A" from a dropdown list and it shows 10 controls, if they choose object B it shows 8 controls, etc. Previously in .net framework, I would use a content placeholder with an ID and then find that ID and start adding controls (controls.Add(newControl)) in the placeholder. It doesn't seem that is an option with .net core. It seems like this would be a common need for various web applications, but I'm not finding many hits.
Another question is whether this can be done in the code behind or if it has to be done on the client-side. If one of the controls in the list is a dropdown, there will be a query that a subroutine will run to get the Key/Value pairs for the dropdown. To me this means it would be more effective on the server side.
I haven't really found any good examples when I do some searching. Can anyone point me to a good resource or provide me with a basic example - either client-side or server-side? Thanks!
There are many options, but I'll describe a simple one, using server side processing. As you explained in your comment, there will be 2 pages:
One that will display the select element that will be used to choose a set of controls.
The page that will be returned according to the previous choise, displaying the selected set of controls.
I assume that you know how to build the first page.
For the second page, you can leverage the ASP.NET Core MVC pattern to achieve the desired result.
You will need the three usual MVC elements:
An Action in a Controler.
A ViewModel for your Razor View.
A Razor View.
The Action does the following:
Receives the id of the selected set of control (via the Action's parameter).
Uses this id to retrieve the information about the corresponding set of controls from your repository.
Builds a ViewModel out of the received information.
Builds a View using the obtained ViewModel.
Return the builded View.
Here is some simplified example code:
In your controller, add the following method:
#!lang-cs
Public IActionResult GetProgramControlSet(int ProgramId)
{
// Here, use the id to get the data from your repository
// that will be used to build set of controls.
// Supposing you have defined a GetControls method,
// it could look like:
var SelectedControls = MyRepository.GetControls(ProgramId);
// If needed, you can build a ViewModel out of the received SelectedControls.
var SelectedControlsViewModel = new ControlSetViewModel(SelectedControls);
return View(SelectedControlsViewModel)
}
Of course, many things are missing here: error handling, etc...
Here is what the ViewModel could be:
#!lang-cs
public class ControlSetViewModel
{
public string Name { get; private set; }
public List<IControl> Controls { get; private set; }
public ControlSetViewModel(...)
{
// Whatever needs to be done to construct the ViewModel
}
}
public enum ControlKind
{
Button,
Select,
Textarea
//...
}
public interface IControl
{
ControlKind Kind { get; }
}
public class ControlButton : IControl
{
public ControlKind Kind => ControlKind.Button;
public string Label { get; set; }
public string Text { get; set; }
public string Color { get; set; }
// ... All other needed properties for the button
}
public class ControlTextarea : IControl
{
public ControlKind Kind => ControlKind.Textarea;
public string Label { get; set; }
public string PlaceholderText { get; set; }
public string RowCount { get; set; }
// ... All other needed properties for the textarea
}
public class ControlSelect : IControl
{
public ControlKind Kind => ControlKind.Select;
public string Label { get; set; }
public string PlaceholderText { get; set; }
public List<SelectOption> Options { get; set; }
// ... All other needed properties for the select
}
public class SelectOption
{
public string Text { get; set; }
public string Value { get; set; }
}
You could also use inheritance instead of interface for the control classes.
Now the view.
It is a Razor page containing something akin to
#model ControlSetViewModel
#*... some HTML ...*#
<div>
<h1>#Model.Name</h1>
#foreach(var control in Model.Controls)
{
<div>
switch(control.GetControlKind())
{
case ControlKind.TextArea:
var Textarea = (ControlTextarea)control;
<label>#Textarea.Label</label>
<textarea rows="#Textarea.RowCount"/>
break;
case ControlKind.Select:
var Select = (ControlSelect)control;
<label>#Select.Label</label>
<select>
#foreach(var option in Select.Options)
{
<option value="#option.Value">#option.Text</option>
}
</select>
break;
#*... etc ...*#
default:
#*... etc ...*#
}
</div>
}
</div>
#*... More HTML ...*#
Of course this is far to be finished. All the infrastructure and code that will actually react to the displayed controls is missing.
Is it a form you that will be posted?
Is it Javascript code that will react to the control manipulation?
Or another mecanism?
This questions will need to be addressed.
In Orchard CMS, I can create a part and weld it to the site using Filters.Add(new ActivatingFilter<TermsAndConditionSettingsPart>("Site")); and have the editor for this part show up in the site settings.
I also have a few pages in my admin screens that I have used controllers and actions to allow the user edit settings for my modules.
I am wondering how I can weld a part onto one of my custom admin pages. I think I need to do something similar to the code mentioned above, but I'm not too sure what I should be welding to (ie- what should I replace "Site" with)? Do I need to create a content type for each of my admin pages?
Any help would be appreciated.
After exploring further and taking into account #Piotr's excellent answer, I've managed to achieve what I wanted to do.
Step 1: Migrations
private readonly IOrchardServices _services;
public Migrations(IOrchardServices services) {
_services = services;
}
public int Create()
{
ContentDefinitionManager.AlterTypeDefinition("PlayerSearch", cfg => { });
var content = _services.ContentManager.New("PlayerSearch");
_services.ContentManager.Create(content);
return 1;
}
In the example above, "PlayerSearch" is the name of my content type (ie the item I will be welding my parts to). This code simply creates a PlayerSearch type and creates a single instance of it which is then persisted.
Step 2: ContentPart
I created a simple POCO class as a ContentPart. This is what I want to weld to my PlayerSearch page:
public class PlayerSearchPart : ContentPart
{
public string String1 { get; set; }
public string String2 { get; set; }
public string String3 { get; set; }
public int Int1 { get; set; }
public int Int2 { get; set; }
public int Int3 { get; set; }
}
Step 3: ContentHandler
The next thing I did, was to weld my PlayerSearchPart to my PlayerSearch type as defined in the migrations:
public class PlayerSearchHandler : ContentHandler
{
public PlayerSearchHandler()
{
Filters.Add(new ActivatingFilter<PlayerSearchPart>("PlayerSearch"));
}
}
I did this in the ContentHandler by using the ActivatingFilter.
Step 4: The Controller
Now we need to create a page in the admin screens that is capable of displaying all the welded parts to the user:
private readonly IOrchardServices _services;
public PlayerManagementController(IOrchardServices services) {
_services = services;
}
[HttpGet]
public ActionResult PlayerSearch()
{
var playerSearchType = _services.ContentManager.Query().ForType(new[] {"PlayerSearch"}).Slice(0, 1).FirstOrDefault();
var model = _services.ContentManager.BuildEditor(playerSearchType);
// Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
return View((object)model);
}
This action retrieves the instance of my PlayerSearch type that I created in the Migrations file, builds the editor for it and then passes this to the view.
Step 5: The View
Our action of course needs a view, and it's pretty simple once you know how:
#using (Html.BeginFormAntiForgeryPost()) {
#Html.ValidationSummary()
#Display(Model.Content)
<fieldset>
<button class="primaryAction" type="submit">#T("Save")</button>
</fieldset>
}
Step 6: The Driver
Nothing out of the ordinary here:
// GET
protected override DriverResult Editor(PlayerSearchPart part, dynamic shapeHelper)
{
return ContentShape("Parts_PlayerSearch_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: "Parts/PlayerSearch",
Model: part,
Prefix: Prefix));
}
// POST
protected override DriverResult Editor(PlayerSearchPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
Step 7: Displaying the editor for your part
The final step here is to create the editor template for your part, and to also ensure that you have a record in your placement.info for the part so that Orchard knows where to render it.
This technique was taken from the way that the site settings work. To have a look at this, and to see how the Post action would work too, have a look at the controller at Orchard.Core.Settings.Controllers.AdminController.
You cannot weld anything onto a custom page - it doesn't work like this.
Parts are welded onto content items, like Site. Site is no different from other content items, like User, Page etc., with one exception - there is always a single Site item per tenant. What you actually see when you go to any section in Settings is an editor for that item (and each section displays a part of that editor that corresponds to a named group). And this functionality is specific for Site items only.
If you need site-wide settings, the best way is to always weld parts onto Site item. Then you can provide links (or tabs etc.) that point to that specific editor from inside your custom views.
I have created a simple C# Windows 8 grid application.
If you're unfamiliar with this layout, there is a brief explanation of it here :
Link
What I would like to have is simple - some custom ItemDetailPages. I'd like to be able to click on some items on the GroupDetailPage and the GroupedItemsPage and navigate to a custom .xaml file, one where I can include more than one image.
I'm sure there is a simple way of doing that that I have missed out on, and I'm also sure that this information will be useful for a lot of people, so I will be offering a bounty on this question.
I have struggled with doing this so far :
I've created a CustomDataItem in the SampleDataSource.cs class :
/// <summary>
/// Generic item data model.
/// </summary>
public class CustomDataItem : SampleDataCommon
{
public CustomDataItem(String uniqueId, String title, String subtitle, String imagePath, String description, String content, SampleDataGroup group)
: base(uniqueId, title, subtitle, imagePath, description)
{
this._content = content;
this._group = group;
}
private string _content = string.Empty;
public string Content
{
get { return this._content; }
set { this.SetProperty(ref this._content, value); }
}
private SampleDataGroup _group;
public SampleDataGroup Group
{
get { return this._group; }
set { this.SetProperty(ref this._group, value); }
}
}
However, obviously, adding to the ObservableCollection
private ObservableCollection<SampleDataGroup> _allGroups = new ObservableCollection<SampleDataGroup>();
public ObservableCollection<SampleDataGroup> AllGroups
{
get { return this._allGroups; }
}
is impossible with a different data type. So what can I do in this case ?
Thanks a lot.
I have a simple grid application; how do I make it possible to have one of the elements in the group item page link to a custom item detail page ?
Ok, lets take the app that is generated when using the "Grid App" template from Visual Studio.
The data class for the elements on the group items page is the SampleDataItem class. What you can do is add some type of data field (bool, int, or other) that indicates how to handle the navigation. In this example, we are keeping it simple, so we add a bool to indicate whether the navigation is custom or not.
public class SampleDataItem : SampleDataCommon
{
// add flag as last param
public SampleDataItem(String uniqueId, String title, String subtitle,
String imagePath, String description, String content, SampleDataGroup group,
bool isCustomNav = false)
: base(uniqueId, title, subtitle, imagePath, description)
{
this._content = content;
this._group = group;
this.IsCustomNav = isCustomNav;
}
// to keep it simple this doesn't handle INotifyPropertyChange,
// as does the rest of the properties in this class.
public bool IsCustomNav { get; set; }
...
}
So when you are adding a new SampleDataItem object to be displayed, you just need to set the isCustomNav field in the constructor.
Now all we have to do is change the already existing click event handler in the grid on the grouped item page (GroupedItemsPage.xaml.cs):
void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
// Navigate to the appropriate destination page, configuring the new page
// by passing required information as a navigation parameter
var item = (SampleDataItem)e.ClickedItem;
var itemId = item.UniqueId;
if (item.IsCustomNav == false)
{
// default
this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}
else
{
// custom page
this.Frame.Navigate(typeof(ItemDetailPage2), itemId);
}
}
All we are doing above is getting the selected item and then testing the navigation flag that we added earlier. Based on this we navigate to either the original ItemDetailPage or a new one called ItemDetailPage2. As I mentioned before, the navigation flag doesn't have to be a bool. It can be an int or enum or some other type that tells us where to navigate.
Note that if you want similar behavior on the GroupDetailsPage, you just have to update the click event handler there the same way.
Hope that helps.
Yes you should be able to create a custom or different data type. If you create a Win8 app using the grid template, you see that the template does three things for you:
1) It creates three types, SampleDataCommon, which is the base, SampleDataItem, which implements SampleDataCommon and adds two new properties - content and group, and SampleDataGroup which also implements SampleDataCommon, adds a method, ItemsCollectionChanged, and adds two properties, Items and TopItems.
2) It creates a class called SampleDataSource, in which a collection of SampleDataGroup is created and named AllGroups: ObservableCollection AllGroups.
3) It binds Items and AllGroups of SampleDataSource to objects in XMAL pages.
In your case, you use the same data structure. In other words, you will create a group with items, etc.
I now have two list box, lstStock and lstSupply. lstSupply consists of
-Pen
-Pencil
-Staple
-Stapler
-Folder
-Notebook
-Eraser
-Marker
and i have two button, one button named btnAdd and another btnRemove.
when i click on btnAdd i want the selected item in lstSupply to be added into lstStock which i have done by
lstStock.Item.Add(lstSupply.SelectedItem);
if i select the same item in lstSupply twice, I want it to be consolidated in lstStock.
for instance, if i select Pen twice, the list box shall give me "Pen x 2" instead of Pen in a line and another Pen in another line.
I got the feeling this is dealing with foreach but i dont really know how to use that.
Okay, so first of all, you're going to need to store something else in lstStock. I'd suggest something like this:
public class StockItem {
public string Name { get; set; }
public int Count { get; set; }
}
Next, I'd suggest that you don't use the .Items field as the actual container where you're storing your stock items. Instead, I'd have a List<StockItem> that you use to populate lstSupply when you refresh.
private List<StockItem> _items = new List<StockItem>();
Then, when you click "add", you do two things:
Iterate through _items to see if there is already a stock item in there of the matching supply. Create one, and add it, if it does not exist.
Find the matching stock item in _items and increment its Count.
To get it to display the way you'd like, we can override ToString() in our StockItem class.:
public class StockItem {
public string Name { get; set; }
public int Count { get; set; }
public override string ToString() {
if (Count > 1)
return Name + " x" + Count;
return Name;
}
}
Finally, whenever any changes are made, you simply re-populate lstStock:
private void RefreshGui() {
lstStock.Items.Clear();
lstStock.Items.AddRange(_items.ToArray());
}
There are plenty of other ways of going about this, but for this simple exercise, this is how I think I would do it.
If you want the code to work without extra coding, you could simply do this:
if (lstSupply.SelectedIndex > -1 && !lstStock.Items.Contains(lstSupply.SelectedItem))
lstStock.Items.Add(lstSupply.SelectedItem);
this way you are sure that you don't add null items and add each item once max.
MonotouchDialog makes it very easy to create UITableView Dialogs, but sometimes questions like that one popup:
MonoTouch Dialog. Buttons with the Elements API
Now, I have a similar problem but quite different:
List<User> users = GetUsers();
var root =
new RootElement ("LoginScreen"){
new Section ("Enter your credentials") {
foreach(var user in users)
new StyledStringElement (user.Name, ()=> {
// tap on an element (but which one exactly?)
}
),
}
navigation.PushViewController (new MainController (root), true);
Now, the second parameter of StyledStringElement's constructor has the type of NSAction delegate, and doesn't take any arguments, now I dunno how to determine exactly which element been tapped.
How to get that?
If it was Tapped then it has been selected. So you should be able to inherit from StyleStringElement and override its Selected method to accomplish the same goal.
e.g.
class UserElement : StyleStingElement {
public UserElement (User user) { ... }
public override Selected (...)
{
// do your processing on 'user'
base.Selected (dvc, tableView, indexPath);
}
}
For Touch.Unit I created a new *Element for every item I had, TestSuiteElement, TestCaseElement, TestResultElement... to be able to customize each of them and adapt (a bit) their behaviour but I did not use this Selected to replace Tapped. You might want to check but it would not fit with your code pattern to create elements.
"...a Flower by any other name?"
If you look closely NSAction's are just delegates. I prefer to pass Action / Func into those params the reference for which is contained within the...container controller.
So lets so you have a UINavigationController that pushes a DialogViewController. When your element is selected you provide the unique user that you've passed to the Element and go from there :-)
public class MyNavController : UINavigationController
{
Action<User> UserClickedAction;
public MyNavController()
{
UserClickedAction = HandleUserClicked;
}
public void HandleUserClicked(User user)
{
...
}
}