How to pass a RenderFragement<object> Template to a Blazor component - devexpress

I'm stucking on one special thing where I try to pass an object of RenderFragment to a dynamically generated component.
I considered this sample from Devexpress https://docs.devexpress.com/Blazor/401753/common-concepts/customize-and-reuse-components
<DxFormLayout>
<DxFormLayoutTabPages>
#renderLayoutTabPage()
</DxFormLayoutTabPages>
</DxFormLayout>
#code {
private RenderFragment renderLayoutTabPage() {
RenderFragment item = b => {
b.OpenComponent<DxFormLayoutTabPage>(0);
b.AddAttribute(1, "Caption", "My tab");
b.AddAttribute(2, "ChildContent", (RenderFragment)((tabPageBuilder) => {
tabPageBuilder.OpenComponent<DxFormLayoutItem>(0);
tabPageBuilder.AddAttribute(1, "Caption", "DynLayoutItem");
tabPageBuilder.AddAttribute(2, "ColSpanMd", 6);
tabPageBuilder.AddAttribute(5, "Template", (RenderFragment<Object>)((context) => ((itemTemplate) => {
itemTemplate.OpenComponent<DxTextBox>(0);
itemTemplate.AddAttribute(1, "Text", text);
itemTemplate.CloseComponent();
})));
tabPageBuilder.CloseComponent();
}));
b.CloseComponent();
};
return item;
}
}
So this is the way they are building a complete new ChildContent Fragment.
my razor file looks like:
[Parameter] RenderFragment<object> DisplayTemplate {get;set;} //pass this Fragement to the dynamic component
protected override void OnInitialized()
{
...
b.AddAttribute(3, nameof(DxDataGridColumn.DisplayTemplate), (RenderFragment<Object>)((context) => ((itemTemplate) =>
{
itemTemplate.AddContent<object>(0, DisplayTemplate, context);
})));
...
}
When I run this, the DisplayTemplate does not get rendered. I only can see the type string "Microsoft.AspNetCore.Components.RenderFragment`1[System.Object]"
What I'm doing wrong here?

Got it!
In my Component I was using this pattern
<MyComponent>
<DisplayTemplate>
#DisplayTemplate
</DisplayTemplate>
<MyComponent>
The only way it works correct is to use the Attributes directly:
<MyComponent DisplayTemplate"#DisplayTemplate" />

Related

Double Clicking on row and getting respective row values

I want to double click on any row to getting respective row values. It is an .Razor page.
First of all, I would create a component that will inherit from QuickGrid, so you can manage it easier in the future.
// CustomGrid.razor.cs
[CascadingTypeParameter( nameof(TGridItem) )]
public partial class CustomGrid<TGridItem> : QuickGrid<TGridItem>, IAsyncDisposable
{
[Inject]
private IJSRuntime JS { get; set; } // inject service in order to use JS interop
// sometimes it is mandatory to override base class parameters by your own
}
// CustomGrid.razor
#using Microsoft.AspNetCore.Components.QuickGrid // move it into the _Imports.razor
#typeparam TGridItem // QuickGrid is a generic-typed component
#inherits QuickGrid<TGridItem> // show inheritance
<div #ref="#_gridRef"> // HTML reference of current element
<QuickGrid TGridItem="TGridItem"
Items="#Items"
ItemsProvider="#ItemsProvider"
ChildContent="#ChildContent"
Class="#Class"
// more parameters... >
</QuickGrid>
</div>
Since there is no built-in functionality for adding your custom logic into QuickGrid, you will need to use some JavaScript interopability. Read about it more in docs here and here.
We need to declare some local variables in our CustomGrid.razor.cs:
private string? _rowValue; // specifies row value of currently double clicked row
private ElementReference _gridRef; // HTML element reference object that will be passed to JS function
private IJSObjectReference? _module; // JS module, a file that contains our JS functions
private DotNetObjectReference<CustomGrid<TGridItem>>? _objRef; // .NET object reference that will be passed to JS function in order to use its C# methods
And override some of the component lifecycle methods:
protected override void OnInitialized()
{
_objRef = DotNetObjectReference.Create( this ); // creates .NET object reference of current component instance
}
protected override async Task OnAfterRenderAsync( bool firstRender )
{
if( firstRender )
{
_module = await JS.InvokeAsync<IJSObjectReference>( "import", "./js/customGrid.js" ); // creates a reference of our JS module
if( _module is not null )
{
await _module.InvokeVoidAsync( "initialize", _gridRef, _objRef ); // calls our JS function and passes some arguments
}
}
}
Now, you need to create a JS module and a functions that will add desired logic for you on the first render of the CustomGrid component, like this:
// wwwroot/js/customGrid.js
export function initialize(customGrid, dotNetObj) {
if (customGrid) { // check if custom grid element exists
var rowValue;
const rows = customGrid.querySelectorAll('tbody > tr'); // get all rows except the header row
for (let i = 0; i < rows.length; i++) {
rows[i].addEventListener('dblclick', (e) => { // add event listener to current row in the loop
rowValue = e.path[1].innerText; // get innerText of current row in the loop
console.log(rowValue)
updateCurrentRowValue(rowValue, dotNetObj); // function that will return the current row value and refresh the UI
});
}
}
}
function updateCurrentRowValue(rowValue, dotNetObj) {
dotNetObj.invokeMethodAsync("UpdateCurrentRowValue", rowValue); // C# method
}
We're almost done here! If you would try to perform double click on the row, you would see an error in the console stating that CustomGrid does not contain a public method called UpdateCurrentRowValue. Let's add it like this:
[JSInvokable]
public void UpdateCurrentRowValue( string rowValue )
{
_rowValue = rowValue; // assign received rowValue from the JS function to our local _rowValue variable
StateHasChanged(); // force UI refresh
}
Now, all you need to do is to display your _rowValue:
// CustomGrid.razor
<div #ref="#_gridRef">
<QuickGrid TGridItem="TGridItem" . . . /> // collapsed for brevity
<p>Current Row Value: #_rowValue</p>
</div>
You will also need to Dispose your newly created objects of _module and _objRef using IAsyncDisposable.DisposeAsync method:
// CustomGrid.razor.cs
async ValueTask IAsyncDisposable.DisposeAsync()
{
if( _module is not null )
{
await _module.DisposeAsync();
}
_objRef?.Dispose();
}
Usage:
<CustomGrid Items="#people">
<PropertyColumn Property="#(p => p.PersonId)" Sortable="true" />
<PropertyColumn Property="#(p => p.Name)" Sortable="true" />
<PropertyColumn Property="#(p => p.BirthDate)" Format="yyyy-MM-dd" Sortable="true" />
</CustomGrid>
That should work. If you will need any help -- don't hesitate to ask!
Remarks:
This is a basic implementation of your request. It doesn't support scenarios when there are more than 1 grid on the page. It might work, but will be buggy, I guess. For that, you will need to add some more code in JS and CustomGrid code-behind. I didn't add it because it would be too much code in one answer (quite a lot of code came out here anyway).
UPD-1:
Removed custom [Parameter]s to override QuickGrid's ones and added a comment.

ObservableChangeSet wait until list is ready before watching

We have a list page where we can enable or disable a thing™ using a <switch /> That thing™ is toggled with an IsActive flag
public class Thing
{
/* ... */
[Reactive] public bool IsActive { get; set; }
}
Given the following change listener, the idea is when the IsActive property changes (user interaction on a toggle switch), we invoke the _saveItemCommand to save the entity with the new IsActiveState.
public ObservableCollection<Thing> DataObjectList {get;} = new ObservableCollection<Thing>();
public MyClass()
{
_saveItemCommand = ReactiveCommand.CreateFromTask(SaveItemInternal);
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_saveItemCommand);
}
public void OnNavigatingTo()
{
var newItems = _myService.GetNewItems();
DataObjectList.AddRange(newItems);
}
public void OnDestroy()
{
_listWatcher?.Dispose();
}
The problem I'm having is that when I setup the list, The command seems to be invoked on the last item in the list immediately after AddRange is called.
I have tried using .Skip(1) without any luck, but one thing that seems to work but is ugly is .Skip(DataObjectList.Length)
How can I make it so that the command isn't invoked until the first time the user toggles the switch? What is the correct way to setup this listener?
Most likely you'll want to add a Where statement to indicate it should only be called on the IsActivated switch.
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.ToCollection()
.Where(x => x.Any(value => value.IsActive))
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_saveItemCommand);
So the two lines I added are
.ToCollection()
.Where(x => x.Any(value => value.IsActive))
The ToCollection() will convert it into an observable list and the Where will restrict your observable to when there is change of the IsActive values.
You may wish to add a FirstAsync() call if you want it to happen only once after the Where() call.
After the comments on Glenn's answer and some additional conversations with Rodney, here's what finally works.
_listWatcher = DataObjectList
.ToObservableChangeSet()
.AsObservableList()
.Connect()
.WhenPropertyChanged(x => x.IsActive)
.Throttle(TimeSpan.FromMilliseconds(250))
.Skip(1)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(x => x.Sender)
.InvokeCommand(_createActivationsInternal);

Use Mapper.Initialize() for multiple mappings

I use AutoMapper V.6.1.1 as a mapper in my ASP.Net project.
Before I had configuration as below:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<A, B>();
cfg.CreateMap<C, D>().ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
});
var mapper = config.CreateMapper();
var var1= mapper.Map<B>(request);
var var2= mapper.Map<List<C>, List<D>>(result);
Now, I want to refactor the code, using Mapper.Initialize(). So I used:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<A, B>();
cfg.CreateMap<C, D>().ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
});
var var1= Mapper.Map<B>(request);
var var2= Mapper.Map<List<C>, List<D>>(result);
I have an run time error:
Missing type map configuration or unsupported mapping. Mapping types: A-> B
Is there any problem with using multiple configurations in Mapper.Initialize? There is no error in the case that has one mapping in Initialize() body. How should I fix the error?
Maybe you have more than one Mapper.Initialize in your project while you should not have multiple Mapper.Initialize in your project else it will become override and you lost previous mapping configurations that you set by Mapper.Initialize. Now It is possible to get the error (Missing type map configuration or unsupported mapping.)
I recommend you to use AutoMapper.Profile. You can warp your mapping configurations in the form of grouped (in separated Profiles) then register all of theme by Mapper.Initialize at once ;)
Look at this example:
public class AB_Profile : Profile {
protected override void Configure() {
CreateMap<A, B>();
// CreateMap<A, B1>();
// CreateMap<A, B2>();
}
}
public class CD_Profile : Profile {
protected override void Configure() {
CreateMap<C, D>()
.ForMember(dest => dest.CityDesc, opt => opt.MapFrom(src => src.City));
}
}
Then initialize the Mapper using above Profiles:
Mapper.Initialize(cfg => {
cfg.AddProfile<AB_Profile >();
cfg.AddProfile<CD_Profile >();
});
Starting version 5 use this, as mentioned on their website...
public class OrganizationProfile : Profile
{
public OrganizationProfile()
{
CreateMap<Foo, FooDto>();
// Use CreateMap... Etc.. here (Profile methods are the same as configuration methods)
}
}
// How it was done in 4.x - as of 5.0 this is obsolete:
// public class OrganizationProfile : Profile
// {
// protected override void Configure()
// {
// CreateMap<Foo, FooDto>();
// }
// }
See Doc
Then initialize the mapping as...
Mapper.Initialize(cfg => {
cfg.CreateMap<Foo, Bar>();
cfg.AddProfile<OrganizationProfile>();
});

After using Angular2+ RouteReuseStrategy, how to call a method in return to cache components to update part?

I have a list page, and click a column to enter the details page. Edit and return. Because using RouteReuseStrategy, it can maintain the list page of the scene remains the same. But I'd like to partially update, However, I don't know how to trigger it.
Here is my RouteReuseStrategy service and most of the same.
export class SimpleReuseStrategy implements RouteReuseStrategy {
_cacheRouters: { [key: string]: any } = {};
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this._cacheRouters[route.routeConfig.path] = {
snapshot: route,
handle: handle
};
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this._cacheRouters[route.routeConfig.path];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this._cacheRouters[route.routeConfig.path].handle;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr:
ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
In your list page component you can listen to route changes:
Detect if the navigation start came from the detail route i.e. /detail/1
if so, get the 'id' param of the route /detail/1
replace that element in the list
// List component
ngOnInit(): void {
this.subscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
// checkIfDetailPage and store ID
}
if (event instanceof NavigationEnd) {
// checkIfThisPage and get the ID
// replace element with ID
}
});
}
An alternative approach for detecting something changed (instead of observing the route changes) you could implement Service to which your list component subscribes and your detail component notifies.

HTTP: getById creates new instance, which is missing an Array (Angular 2 RC1 + TS)

I got some really good help to a previous question at: "TypeError.parent.context.car.getBrands is not a function": s
and that is related to my current question. As can seen in that answer to my previous error, my app won't work, unless I create an new instance of "car", but hen I call that method:
getById(id: string) {
return this.http.get('app/car.json'+id)
/*
if I log the incoming data here to the console,
the correct data from server comes, eg: 'id: id, name: name, brands: Array[2]'
*/
.map(data => data.json())
.map(car => new Car(car.id, car.name)); //<== this line causes problem!
}
receiving component:
routerOnActivate(curr: RouteSegment): void {
let id = curr.getParam('id');
this._service.getById(id)
.subscribe(car => {
// this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car);// <=== correct car, without the array of brands
});
// code here is executed before code from the server arrives
// event though it is written below
}
it creates a new instance "Car". Well that is all good, but the Car also contains an Array of Brands.
My service looks like this:
#Injectable()
export class Service {
constructor(private http: Http) { }
getCars(){
return this.http.get...
}
getById(id: string) {
return this.http.get...
}
}
and my Car class like:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>();
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
So I also have some data in the brands Array, but since the getById method creates a new car, it only takes the parameter id and name, and the brands array becomes empty! I don't know how to get the data from the server side so that it includes the array of brands!
I've (desperately) tried creating an Car in my service, which does log the correct data... but obviously doesn't work.
getById(id: string) {
this.http.get('app/car.json'+id)
.map((res: Response) => res.json())
.subscribe(car => {
//this code is executed when the response from the server arrives
this.car = car;
console.log("res: ", this.car); // <==== correct data!
return this.car;
});
//return this.car placed here doesn't give void error, but returns an undefined car, since the code gets executed before subscribe!
}
and receiving component:
routerOnActivate(curr: RouteSegment){
let id = curr.getParam('id');
this.car = this._service.getById(id); //error: Type 'void' is not assignable to type 'Car'
}
Any advice to give? Thanks!
It's been ages, but I thought I would post the solution to my problem. I had to create a static method to get the app to work. As follows:
getById(id:string)
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(data => Car.carFromJSON(data))
Then in my Car class:
static carFromJSON(json) {
let id = json.id
let name = json.name
let brands: Brand[] =
json.brands.map (brand => new Brand())
return new Car(id, name, brands)
}
You're initializing your car with empty array every time:
export class Car {
private brands: Array<Brand>;
constructor(public id: string, public name: string) {
this.brands = new Array<Brand>(); <-- every new Car() will end up with empty array
}
public getBrands(): Array<Brand> {
return this.brands;
}
//some other methods.
}
You have to extend your constructor with brands:
constructor(public id: string, public name: string, public brands: Brand[]) {}
And then call:
getById(id: string) {
return this.http.get('app/car.json'+id)
.map(data => data.json())
.map(car => new Car(car.id, car.name, car.brands)); // <-- add brands
}

Resources