Side Panes & Custom Pages – Implement a Shopping Cart in MDA

I wanted to have look to the new announced Side Panes in model-driven apps. I’ve read some blogs, but until I don’t try things out, I don’t have the feeling that I really know what’s about. It’s still a preview, but maybe my test will help you get a feeling about what’s possible… and get excited for all those goodies to come.

The first place to start are always the docs:

First of all, let’s see what possibilities we have to open side panes. The second part of the blog is about a use case based on side panes, where the Custom Pages are a great help too.

Side pane API

The announcement allows us to have a look on what we can do with side panes

Developers can open one or more panes on the right/far side of the model-driven app using the Client API.  The panes can contain model-driven app pages like views or forms as well as the new custom page.

Inspecting the page, I can tell that side pane is not an IFrame, like a lot of new emprovements inside Power Apps lately.

Two groups

We can open more pages inside the side panes. They will be shown in the order of the creation, but there can be two groups: the one that with pages that can be closed, and the ones who cannot be closed by the user:

The groups are splitted by a small separator

Each page can have an icon (could be a web resource), and a badge. We are free to decide if the badge gets removed when the page is selected.

Let’s see how to open a side pane containing the list of accounts:

Xrm.App.sidePanes.createPane({
    title: "Accounts",
    imageSrc: "WebResources/sample_reservation_icon",
    paneId: "Accounts",
    canClose: true
}).then((pane) => {
    pane.navigate({
        pageType: "entitylist",
        entityName: "account"
    })
});

The width

If we don’t set the width when opening the side pane, it will be 300 pixels. We can also set the width directly when we open the side pane. But we can set it also later. Here is an example of how to set the width later (in this case with a custom page):

const pane1 = await Xrm.App.sidePanes.createPane({
    title: "Reservations",
    imageSrc: "WebResources/sample_reservation_icon",
    paneId: "CustomPage",
    canClose: false, 
    width: 400
})

 pane1.navigate({
        pageType: "custom",
        name: "diana_deliverycalendarpage1_567e2"
    })
pane1.width = 600

But what happens if the side panes that are opened in the application have different width?

Each page has it’s own width

Collapsed/Expanded

We have the possibility to set the expand or collapse state

//expand
Xrm.App.sidePanes.state = 1
//collapse
Xrm.App.sidePanes.state = 0

Navigation inside side panes

The navigation stays inside the side pane. We can navigate between forms, views , web resources and custom pages (at least). The history is preserved. The only one who breaks outside the area, is the lookup dialog. This will be opened on the whole screen.


We can also open the form in a “new window”. In that case, the page incide the side pane, this will navigate to the entitylist .

Client API Interaction with the side pane

Before we create a pane, we should check if the pane is already created. It doesn’t matter when or where the pane was created.

So, it the getPane() returns null, we can go on and create the pane using the “async/await” pattern. After that we can navigate to the page (in the example, to a custom page)

async function dotTheJob(){  
    const paneConfig = {
        title: "Basket",
        imageSrc: "WebResources/sample_reservation_icon",
        paneId: "Basket",
        canClose: true
    };
    const pane = Xrm.App.sidePanes.getPane("Basket") ?? await Xrm.App.sidePanes.createPane(paneConfig); 
    pane.navigate({
        name: "diana_sidepanecustompage_d4eb4",
        pageType: "custom"
    });
}

Once we have the pane, we can close, select or navigate to the page inside the pane. We cannot call any function inside the page opened in the side pane. We don’t get a way to interact with the content that is loaded in the page. But we are still pretty powerful, since we have the “navigate()” method. This way we can change the content of a page.

For instance, a script on onLoad of a form (let say Order), could open the corresponding account on the right side (here the code):

async function navigateToAccount(){
const pane = Xrm.App.sidePanes.getPane("Account")?? await Xrm.App.sidePanes.createPane({
    title: "Account",
    paneId: "Account",
    canClose: true
});
 pane.navigate({
        pageType: "entityrecord",
        entityName: "account",
        entityId : Xrm.Page.getAttribute("diana_accountid").getValue()[0].id
    })
}

I think the most powerful property of the side panes is that we have access from everywhere in the app. All we have to know is the paneId.

There are a few more interesting properties for panes (like alwaysRender or keepBadgeOnSelect). You can find them in them in the docs. But for now let’s have a look to a use case where the combination of side pane and custom pages are bringing some magic to model driven apps.

Implementing a shopping basket

Let’s suppose that the user has to plan equipment deliveries for his/her company. Or maybe the user is searching for products to buy. The classical way in model-driven apps, would be to create a new order, save it, and inside the order form to add the products one by one (using the product dialog). If you want to have some other ways to search for products, you would need to open another window, or find some other ways to search.

Let’s see how a side pane can help. As I said, I find the most interesting property of the side panes, that we can grab the page just by knowing the paneId. So we can access it from everywhere in the app. It could be from the product list, product form, or maybe by searching inside an older delivery. We just add more products to the basket, and when we’re done, we can just generate the order.

Let’s have a look how it world look like. (I know, the UX doesn’t look too professional. A few improvements would be nice. I’ve still considered that it could be an example to show how it works, so I’ve wanted to share it with you).

Technical architecture

We first have to pick an id for the side pane page. We’ll access it from the ribbon,or everywhere where we want to add products to the shopping basket.

Since we cannot directly access the side pane, I’ve created a dedicated table “Shopping Basket”. I’ll use it to add all the products, until the user is ready. It’s good that the basket is saved. If the user decides to close the application, he/she can go on next time.

Each time we add a new product in the basket, we can show a badge on the side-pane, containing the count of products in the basket.

Now we can choose what we want to show in the side pane:

  • it can be an editable grid, where we can change the amount, delete the records, and make a ribbon button to generate the order
  • it can be a custom page

I’ve decided to go with the custom page. I had two reasons for the Custom Page:

  • I’m free to make it more “fancy”, more tailored to the needed user experience; and can add images
  • For a view, I must show the view “My shopping baskets”, because I should see my own basket. By showing the “entitylist”, the user is free to switch the views, so depending on the right he/she could see there products from another basket.

When the user is ready to generate the order, we have a button where we need to:

  • generate an order
  • adds all products from the basket to the orderproducts of the order
  • then delete all products from the basket
  • navigate to the order. That way the user can input more data in the order form, and we don’t need to create another user interface for that

Ribbon implementation (Add to basket)

To make the ribbon button, I’ve used the new commanding for main grid/form. Unfortunately I don’t know a way to add pages to the side panes using low code. So I’ve took the JavaScript way.

For that I’ve edited the app using the new solution experience, and I’ve chosen the “…” to edit the command bar (preview)


There I’ve added a button, and took the “Action”: “Run Javascript”.

The parameters are self-explaining, The interesting part is the library that I’ve used to create the side pane, add the products and refresh the custom page:

Below is the script as a text :

function AddToShoppingBasket(formContext, productIds){
  const productId = productIds?.length>0 ? productIds[0].replace("{","").replace("}","") : null;
  if(productId!=null){
    dotTheJob(productId);    
  }
     
}

async function dotTheJob(productId){
    //get or create the pane
    const pane = Xrm.App.sidePanes.getPane("Basket") ?? await Xrm.App.sidePanes.createPane({
        title: "basket",
        imageSrc: "WebResources/sample_reservation_icon",
        paneId: "Basket",
        canClose: true
    }) 
    pane.navigate({
        name: "diana_sidepanecustompage_d4eb4",
        pageType: "custom"
    });
    pane.width = 500;   
    //generate an entry in the table Shopping Basket
    const created = await Xrm.WebApi.createRecord("diana_shoppingbasket", { "diana_productid@odata.bind" : "/sample_products(" + productId +")" , "diana_amount": 1});
    //fetch to retrive the count of the entries in the basket (using the aggregate is faster)
    const fetchXml = ["<fetch aggregate='true'>", 
        "<entity name='diana_shoppingbasket'>", 
        "<attribute name='diana_shoppingbasketid' alias='count' aggregate='count' />", 
        "<filter><condition attribute='ownerid' operator='eq-userid' /></filter>",
        "</entity></fetch>"].join("");
   const resp = await Xrm.WebApi.retrieveMultipleRecords("diana_shoppingbasket", `?fetchXml=${fetchXml}` );
   const count = resp.entities[0].count;
    //setting the count in the pane badge
    pane.badge = count;
    //this will keep the bade also if the page is selected. Good or bad?
    pane.keepBadgeOnSelect = true;       
}

We can now “save and publish”. I love that it’s only a button which includes the publishing, and that is not taking an eternity to publish :-). Even if sometimes the ribbon doesn’t get rendered, and I need to refresh. But it’s a preview, it’ll get better.

We can proceed the same with the other ribbon buttons, using the same library (or similar).

Implementing the Custom Page

I have a vertical container, a button and a gallery.

My data sources:

For the gallery I’ve used the view MyBaskets, so I get only the items I own.

OnChange of the “Amount”, I make an update on my table

On increment or decrement the amount, is of course similar, just that the Amount is

Value(TextBox1.Value + 1) or Value(TextBox1.Value – 1)

I also have a delete button for each item, which is visible only on hover:

OnSelect: Remove(ShoppingBaskets, Gallery4.Selected)

The interesting part is the action for “Create order” button (I hope I didn’t break any best practices rule, since I’m not that experienced in PowerFx. But I’m definitely working to get better).

ClearCollect(colParent, Patch(Orders, Defaults(Orders), {name: "From basket"}));
ForAll(Gallery4.AllItems, Patch(OrderProducts, Defaults(OrderProducts), 
     {Product: ThisRecord.ProductId, 
     count: ThisRecord.Amount, 
     Order: First(colParent), 
     name: ThisRecord.ProductId.Name}));
ForAll(Gallery4.AllItems, RemoveIf(ShoppingBaskets, ShoppingBasket = ThisRecord.ShoppingBasket));
Notify("Create order, remove shopping basket");
Navigate(First(colParent));

Since I’m in a custom page, I can “Navigate” to the new created order, and let the user make more changes.

Conclusion

That’s it! I love the possibilities that are added to the platform. Step by step we’ve got pro dev and low code power, and the possibility to use them together: PCF, Custom Pages, Commanding, Side Panes, In-Page Notification. WOW!

Of course, there can be made a lot of improvements to this demo. The first would be to make it look better. And make it possible to add more products to the basket at once.

We could design several baskets. For that, when we add a product to the basket, we could show a Custom Page as a dialog, and grab the name of the basket (one of the existing, or just input a new name).

What do you think?

Photo by Kaboompics .com from Pexels

4 thoughts on “Side Panes & Custom Pages – Implement a Shopping Cart in MDA

Add yours

  1. A great article. I have some experience on the side panel but this article is a great source to learn side panel compared to my experience. Thanks, Diana.

    1. Hi Raman,
      Side Panes can show views, forms or CustomPages. PCF can be included in all of these.
      So you cannot show stand-alone PCFs, but you can choose how the PCFs should be included in these containers.
      If you need, a PCF can be the only element on your form/CustomPage/view. The tricky part is if you want to have a PCF full-screen on the form, but you can find details in my other blog: https://dianabirkelbach.wordpress.com/2021/04/28/single-component-tabs-in-model-driven-forms/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Powered by WordPress.com.

Up ↑

%d bloggers like this: