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:
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?
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
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.
Thank you so much for the nice words! Glad to hear that!
Great article. I was wondering if there’s a way to add a custom PCF to the side pane.
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/
Great article Diana! I am new to powerapps and Dynamics. This write is a life saver. I am building a model driven app that needs a cart for products that have to be shipped to sites and this helps me to define the whole setup and keeps the users on on page. I really appreciate you taking the time to do this.
Happy to hear that 🙂
Hi Diana, great article and one I keep coming back to for tips on creating side panes. One thing I’m stumped on is how to pass parameters to a custom page opened as a pane. I’ve tried using the data parameter for pane.navigate but no matter what I try I get the error: “Invalid input to custom page, input needs to be an object”. Any ideas?
Hi Simon,
You should be able to pass params just like the navigateTo function (https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-navigation/navigateto?WT.mc_id=BA-MVP-5004107#custom-page).
For a CustomPage you could try something like this:
pane.navigate({
name: “diana_sidepanecustompage_d4eb4”,
pageType: “custom”,
entityName: “product”,
recordId: productId
});
If you need to pass more than only one parameter, it’s a little tricky. In that case you can pass all parameters inside the recordId string, like Mehdi showed us in his blog: https://xrmtricks.com/2021/10/25/how-to-pass-an-object-from-a-model-driven-to-a-custom-page/
Hope this helps!