PDA

View Full Version : Partial Rendering



plok77
May 18, 2021, 11:52 AM
I'm trying to figure out how to do Partial Page Rendering in Ext.NET 7, as per the example on the 5.3 examples site: https://mvc5.ext.net/#/Dynamic_Partial_Rendering/Partial_Content/

Can a Razor Page be used as the source for loading partial content? I have created a Razor Page with Ext.NET markup in the .cshtml file, and whose code-behind can handle GET requests. However, when this page is requested via a ComponentLoader, no script content is generated by the page. All that is returned is the following markup:


<div id="App.ctl01_Container">



</div>

My Razor Page:

BookingsByMonth.cshtml


@page
@model TMSWeb.ExtNETCore.Pages.Auth.Dashboard.Widgets.Boo kingsByMonthModel
@{
}
<ext-cartesianChart>
<store>
<ext-store data="Model.ChartData">
<fields>
<ext-datafield name="month" />
<ext-datafield name="bookings" />
</fields>
</ext-store>
</store>
<axes>
<ext-numericAxis position="Left"
fields="bookings"
grid="true"
title="Bookings"
minimum="0" />
<ext-categoryAxis position="Bottom"
fields="month"
title="Month" />
</axes>
<series>
<ext-barSeries highlight="true"
xField="month"
yField="bookings" />
</series>
</ext-cartesianChart>

BookingsByMonth.cshtml.cs


using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TMSWeb.Interfaces.NETCore.CQRS;
using TMSWeb.Models.NETCore.Dashboard;
using TMSWeb.Queries.NETCore.Dashboard;

namespace TMSWeb.ExtNETCore.Pages.Auth.Dashboard.Widgets
{
public class BookingsByMonthModel : PageModel
{
private readonly IQueryHandler<GetBookingsByMonthQuery, List<BookingsByMonth>> _queryHandler;

public BookingsByMonthModel(IQueryHandler<GetBookingsByMonthQuery, List<BookingsByMonth>> queryHandler)
{
_queryHandler = queryHandler;
}

public List<object> ChartData { get; set; }

public async Task OnGetAsync()
{
ChartData = (await _queryHandler.HandleAsync(new GetBookingsByMonthQuery(2020)))
.Select(obj => new { Month = obj.MonthName, Bookings = obj.CountOfBookings })
.OfType<object>()
.ToList();
}
}
}

If not, how can I return an Ext.NET PartialViewResult from a controller action in Ext.NET 7.2? I can't find this type when browsing through Intellisense.

Thanks

Paul

fabricio.murta
May 18, 2021, 5:24 PM
Hello Paul!

I am afraid you are looking at the wrong place to base off an Ext.NET 7 partial page. While for most examples that would be the way to go, albeit the "partial" concept itself is the same between versions, the actual workflow has changed considerably and they are not really interchangeable.

A good example on how partials are used in Ext.NET 7 is in the new project template itself, and we have a dedicated blog post about the templates and partial component at Ext.NET Blog: Ext.NET Classic 7.1 with new Partial and Section components (https://ext.net/ext-net-classic-7-1-with-new-partial-and-section/). Please check it out and if you still have questions on how to use the ext-partial component, feel free to post a follow-up.

Hope this helps!

plok77
May 19, 2021, 11:42 AM
FabrÃ*cio

Thanks for your reply. I read the blog post about Partial components that you linked to, however, I can't see how I can use them to achieve what I am trying to do.

I'm trying to create a dashboard composed of different 'widgets', with each widget being a panel containing some combination of charts and data. Each widget will operate independently of others, and the user should be able to customise which widgets are displayed on their dashboard.

Based on the blog post, it looks as if <ext-partial> components need to be added to the view and configured at design time. This won't work for me, as I don't know at design-time exactly which partial views will be displayed on a user's dashboard. This is why the Ext.NET 5.3 Dynamic Partial Rendering example (https://mvc5.ext.net/#/Dynamic_Partial_Rendering/Partial_Content/) was of interest, as it would allow me to defer the decisions about what content to render until run-time. Ideally, I want to make use of ComponentLoader to load the content for each widget from a URL which can be set at run-time. The 5.3 example has a controller action called AutoLoadPartialView which returns an object of type PartialViewResult. As I mentioned in my previous post, I can't find the type PartialViewResult in the V7 framework.

Is it possible to do Dynamic Partial Rendering with Ext.NET V7? If so, can you give me some advice on how to do this? I see that this Github (https://github.com/extnet/Ext.NET/issues/1819) issue says that 'Dynamic loading of partial views is under development...'. Is this something that you plan to release soon?

Thanks

Paul

fabricio.murta
May 20, 2021, 8:50 PM
Hello @plok77!

You can conditionally render parts of pages through several options, they may not necessarily translate into "dynamic partial rendering", but I believe the concept allows you to do what you want.

I have explored some options in a post some time ago, and it may be just what you needed to know to implement what you need. Take a look at the post:
- Post #6 in How to implement alternative to Html.RenderAction (https://forums.ext.net/showthread.php?63053-How-to-implement-alternative-to-Html-RenderAction&p=286348&viewfull=1#post286348)


As I mentioned in my previous post, I can't find the type PartialViewResult in the V7 framework.

Yes, it is expected you won't find it. Unfortunately, Ext.NET 5 and 7, for a number of reasons, are not identical APIs, there's not much we can do as the technology change between the frameworks is considerable. We can't tell it will never be supported like it was in Ext.NET 5, but currently the ways to dynamically render parts of pages is different, using specific .NET Core features, like Tag Helpers.

Hope this helps!

plok77
May 25, 2021, 2:10 PM
FabrÃ*cio

Thanks, I tried your approach using View Components and this works. One further question: can Ext.NET partial and section components be created in code-behind? Or can they only be added statically to a view using tag helpers?

Regards

Paul

fabricio.murta
May 25, 2021, 5:37 PM
Hello again, Paul!

I believe yes, you can do it by "code behind". But it all depends on what you want to achieve. As per your first post, what you want is to add a chart (defined in markup in a partial view) to a "main view", depending on some condition?

Then, reserving a section in the main view is not desirable? How you intend to determine the location where the chart should be drawn? Even in the example you have been using as a base, there's ViewContainer1 to hold the contents of the dynamically added content. In case you are coming from Ext.NET 5 or older, can you point an example that uses partial views more-or-less the way you want it to?

Maybe what you want could be achieved using Loaders; that is, if you don't really want to determine the partial view using markup, but just write the components in C#.

Looking forward to your follow-up.

plok77
May 26, 2021, 3:49 PM
FabrÃ*cio

The following two pages from the 5.3 Examples website were of interest:

https://mvc5.ext.net/#/Viewport_Basic/Built_as_Class/
https://mvc5.ext.net/#/Dynamic_Partial_Rendering/Partial_Content/

I had thought that I could use the 'Built as Class' approach to dynamically create the panels for the viewport, then use loaders to load their content.

However, in the 'Partial Content' example, the controller actions have a return type of 'PartialViewResult'. Correct me if I'm wrong, but this type is no longer available in Ext.NET v7? If so, how can dynamic content be loaded into a panel from either an MVC view or Razor Page?

Thanks

Paul

fabricio.murta
May 27, 2021, 1:17 AM
Hello again, Paul!


I had thought that I could use the 'Built as Class' approach to dynamically create the panels for the viewport, then use loaders to load their content.

Well, not with the partial and section implementation in Ext.NET, no.


Correct me if I'm wrong, but this type is no longer available in Ext.NET v7?

You are not wrong. This type is not available in Ext.NET v7. "No longer", though, is not really true, as Ext.NET 7 is a rewrite from scratch and not a "port" of Ext.NET 5 to .NET Core. That said, functionality not present in v7, that was available in previous versions, simply couldn't be implemented due to technology or time constraints. The GitHub issue #1819 (https://github.com/extnet/Ext.NET/issues/1819) is logged exactly to track and implement this feature.


If so, how can dynamic content be loaded into a panel from either an MVC view or Razor Page?

Now comes the fun part.

Let's start simple; if you want to just draw some component into a div, you can simply "render it to" that div. Take a look at this example where we render a grid panel to a reserved div in the page: Grid Panel > Array Grid > DirectEvent Creation (https://examples.ext.net/#/gridpanel/arraygrid/directevent_creation).

Okay, but that's just not enough, right? You want a viewport, and you can't simply renderTo content into it; you'd rather ViewPort.add(component). And here's what to it:

view: t63140_loader.cshtml



@page
@model ExtNet7Playground.t63140_loaderModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>
Dynamic GridPanel added to viewport via DirectEvent - Ext.NET Examples
</title>
<script>
// auxiliary script for the to-be-rendered grid, just for additional effects
var template = '<span style="color:{0};">{1}</span>';

var change = function (value) {
return Ext.String.format(template, (value > 0) ? "green" : "red", value);
};

var pctChange = function (value) {
return Ext.String.format(template, (value > 0) ? "green" : "red", value + "%");
};
</script>
</head>
<body>
<h1>
Dynamic GridPanel added to viewport via DirectEvent
</h1>

<ext-viewport layout="Border" id="Viewport1">
<items>
<ext-panel region="North">
<tbarItems>
<ext-button text="Add GridPanel"
iconCls="x-md md-icon-add-circle-outline"
onDirectClick="Button1_Click"
marginAsString="0 0 20 0">
<directEvents>
<click pageHandler="Button1_Click">
<extraParams>
<ext-add key="button" value="this" mode="Raw" />
<ext-add key="target" value="App.Viewport1" mode="Raw" />
</extraParams>
</click>
</directEvents>
</ext-button>
</tbarItems>
</ext-panel>
</items>
</ext-viewport>
</body>
</html>


model: t63140_loader.cshtml.cs (to keep it short, I added references to unchanged code you should grab off the example above)


using Ext.Net;
using Ext.Net.Core;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ExtNet7WebPages_NuGet.Pages.Forums._6._3._0_2
{
public class t63140_loaderModel : PageModel
{
public void OnGet()
{

}

public IActionResult OnPostButton1_Click(Button button, Container target)
{
var grid = this.BuildGridPanel();

grid.Region = RegionType.Center;

target.Add(new List<Component>() { grid });

// Disable the Button
button.Disabled = true;

return this.Direct();
}

private GridPanel BuildGridPanel()
{
// please copy-paste it from the example linked above
}

private Store BuildStore()
{
// please copy-paste it from the example linked above
}

List<object> Data = new List<object>
{
// please copy-paste it from the example linked above
};
}
}


So, with this, you could add anything to the viewport the way you wanted!

Well, except for one part we don't show in this example:


then use loaders to load their content

That's just a matter of adjusting BuildStore() or BuildGridPanel() (say, if instead of a grid panel you wanted a loader to fill a panel) to implement loaders. I suggest you try this simpler approach first, and then develop into the loaders once you can make the page load the panels into the viewport dynamically as required. The right loader will depend of how and what you want to fill in the panels.

- For store remote loading you want this example: Grid Panel > Paging and Sorting > Page (https://examples.ext.net/#/gridpanel/Paging_and_Sorting/Page)
- For panel content loaders: Add Loader to a panel created Dynamically (https://forums.ext.net/showthread.php?62353) (this is for v5, but assigning loader from the model in v7 shouldn't be very far from it)

Hope this helps!

fabricio.murta
May 27, 2021, 10:51 PM
Hello again, Paul!

We have just added an example to the Examples Explorer project using Loaders. It explores the feature basically from markup, but that'd be a step closer for you to envise your case with panels with loaders written from code behind.

The example is currently only available at the GitHub repository, but soon it will be live in Ext.NET 7 Examples Explorer (https://examples.ext.net).

Here's the example: Loaders > Basic > Markup example in Examples Explorer 7 project (https://github.com/extnet/examples.ext.net/tree/dev/src/Pages/samples/loaders/basic/markup).

Hope this helps!

plok77
May 28, 2021, 8:59 AM
Thanks, I'll take a look at the examples you have mentioned

plok77
Jun 11, 2021, 4:39 PM
I tried the approach of using a loader, as per the example recently added to the Ext.NET 7.2 Examples Explorer. However, I'm having a problem when one of the controls that I am attempting to dynamically load is a chart. I get the following error in the JS console:


Unrecognized class name / alias: widget.cartesian

I believe this error is occurring because the charts.js script isn't being loaded onto the page. When I was adding charts via markup this script was being automatically loaded. I assume that's because, when adding charts at design time, the framework knows that the the charts.js script needs to be loaded. What's the best approach to load this script when adding charts dynamically to a page? Just hard-code the script link in the page? Or is there a way to signal to the Ext.NET framework that it needs to load the script?

fabricio.murta
Jun 11, 2021, 7:45 PM
Hello @plok77!

If you don't want the resources to be allocated unless the chart is to be actually used, an alternative would be to use the frame approach to the loader.

If you don't mind having the charts resources always loaded, manually adding them is one of the options. This shouldn't pose a mid-term issue as the link does not tend to change -- unless theme switching is a thing in the project in question.

Similar to the alternative above, it is possible to just reference the base chart (or any chart-related component, in fact) from the OnGet() method in the model code, and the related resources will be included for you without any other "preload" or "unused code" in the initial page script. For instance:



public void OnGet()
{
this.GetCmp<CartesianChart>(String.Empty);
}


The code above would make it so the chart resources are included although there's no chart in the page initially.

Unfortunately you need to know what you may use in the page beforehand; this is not something specific to Ext.NET 7, but also in some circumstances even Ext.NET 5- couldn't tell it was supposed to load resources.

You may though, add custom scripts to make the right includes if you have some code to do so, via the this.X().AddScript() method you can call from any direct event or method; but that'd be up to you to load the right resources and deal with potential side effects.

Hope this helps!

plok77
Jun 14, 2021, 11:43 AM
Thanks, adding
this.GetCmp<CartesianChart>(string.Empty); ensured that the chart script was loaded. The page will always contain at least one chart, so this seems like the best approach.

However, have run into a problem with binding a chart to a store. I have the following Razor page that is returning component content to a parent page.


public class BookingsByMonthModel : PageModel
{
private readonly IQueryHandler<GetBookingsByMonthQuery, List<BookingsByMonth>> _queryHandler;
private readonly ISerializationService _serializationService;
private const string StoreId = "bookingsByMonthStore";

public BookingsByMonthModel(
IQueryHandler<GetBookingsByMonthQuery, List<BookingsByMonth>> queryHandler,
ISerializationService serializationService)
{
_queryHandler = queryHandler;
_serializationService = serializationService;
}

public async Task<IActionResult> OnGetAsync()
{
var store = new Store
{
Data = (await _queryHandler.HandleAsync(new GetBookingsByMonthQuery(2020)))
.Select(obj => new { Month = obj.MonthName, Bookings = obj.CountOfBookings })
.OfType<object>()
.ToList(),
StoreId = StoreId,
Fields = new List<DataField>
{
new DataField { Name = "month" },
new DataField { Name = "bookings" }
}
};

var panel = new Panel
{
Layout = LayoutType.Column,
Items =
{
new Panel
{
ColumnWidth = 0.6,
Items =
{
new CartesianChart
{
Width = 600,
Height = 300,
Store = StoreId,
Axes = new List<Axis>
{
new NumericAxis
{
Position = Position.Left,
Fields = "bookings",
Grid = true,
Title = "Bookings",
Minimum = 0
},
new CategoryAxis
{
Position = Position.Bottom,
Fields = "month",
Title = "Month"
}
},
Series = new List<Series>
{
new BarSeries
{
Highlight = true,
XField = "month",
YField = "bookings",
Tooltip = new SeriesTooltipModel
{
TrackMouse = true,
Width = 140,
Height = 28,
Renderer = "function (toolTip, storeItem) { toolTip.setTitle(Ext.String.format('{0}: {1}', storeItem.get('month'), storeItem.get('bookings'))); }"
}
}
}
}
}
},
new Panel
{
ColumnWidth = 0.4,
Items =
{
new GridPanel
{
Width = 500,
Height = 300,
Frame = true,
Store = StoreId,
Columns = new List<Column>
{
new Column { Text = "Month", DataIndex = "month", Flex = 1 },
new NumberColumn { Text = "Bookings", DataIndex = "bookings", Format = "0" }
}
}
}
}
}
};

var serializedControls = new StringBuilder();

using var writer = new StringWriter(serializedControls);

_serializationService.Serialize(new object[] { store, panel } , writer);

return Content(serializedControls.ToString(), "application/json", Encoding.UTF8);
}

The following JSON content is returned by the page:


[{storeId:"bookingsByMonthStore",type:"store",fields:[{name:"month",type:"auto"},{name:"bookings",type:"auto"}],data:[{"month":"Jan","bookings":23},{"month":"Feb","bookings":34},{"month":"Mar","bookings":58},{"month":"Apr","bookings":45},{"month":"May","bookings":45},{"month":"Jun","bookings":1},{"month":"Jul","bookings":21},{"month":"Aug","bookings":16},{"month":"Sep","bookings":0},{"month":"Oct","bookings":1},{"month":"Nov","bookings":9},{"month":"Dec","bookings":4}]},{xtype:"panel",items:[{columnWidth:0.6,xtype:"panel",items:[{height:300,width:600,xtype:"cartesian",axes:[{fields:"bookings",grid:true,minimum:0,position:"left",title:"Bookings",type:"numeric"},{fields:"month",title:"Month",type:"category"}],series:[{highlight:true,tooltip:{height:28,width:140,xtype :"tooltip",trackMouse:true,renderer:function (toolTip, storeItem) { toolTip.setTitle(Ext.String.format('{0}: {1}', storeItem.get('month'), storeItem.get('bookings'))); }},type:"bar",xField:"month",yField:"bookings"}],store:"bookingsByMonthStore"}]},{columnWidth:0.4,xtype:"panel",items:[{frame:true,height:300,width:500,xtype:"gridpanel",columns:[{flex:1,xtype:"gridcolumn",dataIndex:"month",text:"Month"},{xtype:"numbercolumn",dataIndex:"bookings",text:"Bookings",format:"0"}],store:"bookingsByMonthStore"}]}],layout:"column"}]

When the page is being rendered, I get the following error reported in the browser console:


Cannot read property 'getData' of undefined

I can see that this error is occurring because there's no store bound to the chart. If I change my Razor page code to create the store inline within the chart, it works. However that means that I can't use one store as the data source for both the chart and grid. The grid renders fine using the external store id, it's only the chart that won't render.

How do you suggest I can configure my page so that both the chart and grid can share the same store and both can render successfully?

Thanks

Paul

fabricio.murta
Jun 16, 2021, 1:22 AM
Hello again Paul!

This really looks strange as per your description, you shouldn't be getting the store if you already have it, and the returned chart should just have a reference to the already existing store allocated for a grid panel in the page.

The code snipped you shared looks right, I can't tell by that what's wrong with the whole logic.

If you are stuck on that, please provide a code sample so we can reproduce and advise with the right approach to take. This should be possible if we understand the requirements right, so there's probably a small review to the page logic before it can work.

In general, if you already have the store defined, you shouldn't return the full store again, nor define it again; all that's necessary is to create the chart with the ID of the right store (as it looks you already done), and ensure the store is really available whenever the chart can be requested (so you may need to pass a "signal" if the store is not loaded for the model to also create an instance for it client side.

Looking forward to your follow-up!

plok77
Jun 18, 2021, 8:19 AM
Thanks, I was able to work around the problem by defining the store inline in the chart, and assigning it a store id so that it could be referenced by the grid. This solution works so I think I'll leave it at that.

fabricio.murta
Jun 18, 2021, 6:00 PM
Hello again, Paul!

Glad our last post helped you, and thanks for the feedback!