I followed the "Developing Custom Form Control" in kentico documentation and built a custom list box. I added the list box dynamically on the code behind and NOT adding it directly on the code front (ascx). I use the list box on one of my web parts and everything works well when I selected multiple items. However, when I click to edit the web part, all of the selected items are gone and the the list box comes back to its original form ( no item selected ). Therefore, I wonder how kentico save the old data of the form control in the web part.
On the code below, I recreate my scenario with a short version. I dynamically add the list box under a panel.
protected void EnsureItems()
{
// Create item and list box
ListBox tab = new ListBox();
ListItem item = new ListItem();
item.Text = "test";
tab.Items.Add(item);
panel.Controls.Add(tab);
}
protected void Page_Load(object sender, EventArgs e)
{
EnsureItems();
}
Each Form Control should be inherited from FormEngineUserControl. And Kentico utilizes Value property then to store and retrieve values from the db. Here is the example:
public override object Value
{
get
{
return listBox.SelectedValue;
}
set
{
listBox.SelectedValue = ValidationHelper.GetString(value, string.Empty);
}
}
Basically, your getter should return some value to be stored in the database. And in the setter you should initialize your listbox, fill with data and make a selection base on value coming from the database.
Basically, a form control itself doesn't save data to the database. The form control is attached to some form and the form saves the data to the database. Check out the documentation regarding custom form controls.
Related
I'm using Acumatica 2020R1 and I'm adding side panels. Is there anyway, for example on the PO form (PO301000), that I could link a side panel to update when clicking on the Document Details?
I actually discovered a way to do this on Acumatica 2020R1. Here's how I did it:
Edit: I did this for the SOOrderEntry screen, creating a side panel that looks at the SOOrder but uses parameters from the SOLine. This method will work for any two tables with a master-detail relationship by replacing SOOrder & SOLine accordingly.
Part A:
Create virtual fields on the SOOrder DAC that correlate to the SOLine Parameters you need
Update these as lines are selecting/updated using event handlers
pass these fields as the params to your side panel actions as you normally would
While It seems like this alone would work, the side panel actually doesn’t refresh when a new SOLine is selected. We’re going to have to get creative to resolve this issue.
Part B:
Create an additional virtual field on the SOOrder DAC to keep track of which side panel was last opened (I will explain how to track this later). I use a string field to do so. This is especially important if you have more than one side panel on your screen.
Next you want to define a standard graph action that will update this field. Make it Visible = false, since you don’t actually want this button to show. We will call if from behind the scenes later. This is the method I use:
public PXAction<SOOrder> SidePanelCallback;
[PXButton]
[PXUIField(DisplayName = "SidePanelCallback", Visible = false)]
public virtual IEnumerable sidePanelCallback(PXAdapter adapter)
{
SOOrder doc = Base.Document.Current;
if (doc == null) return adapter.Get();
var docExt = doc.GetExtension<SOOrderExt>();
docExt.UsrSidePanelDetailParamsRefresh = adapter.CommandArguments;
return adapter.Get();
}
*Note: docExt.UsrSidePanelDetailParamRefresh is the variable I for the previous step
Now you need a way to trigger your code when your side panel actions are called. To do this we’ll use the following JavaScript
function commandResult(ds, context) {
var ds = px_alls['ds'];
switch (context.command)
{
case ('JustAction'):
ds.executeCallback('GraphAction', 'parameter value');
break;
}
}
You want to replace JustAction with the internal name of your side panel action. it should look like this: NavigateToLayer$DB000028
where DB000028 is the target ScreenID for your sidepanel. If you want to make sure your actions follow the same naming convention, you can check it by adding breakpoints and checking with the debugger.
You should replace GraphAction with the name of the graph action you created in the previous step. In our case SidePanelCallback
Then finally for the parameter value pass whatever you’d like to use to identify the opened side panel. This will be stored in UsrSidePanelDetailParamRefresh, or your equivalent.
You can create a case statement for each side panel you want this functionality for, so you only have to do all this once per graph!
Then finally, attach your script to the client event ‘commandPerformed’ on your data source.
Next we will define another graph action to do the actual refresh of the side panel. Mine looks like this:
public PXAction<SOOrder> SidePanelRefresh;
[PXButton]
[PXUIField(DisplayName = "SidePanelRefresh", Visible = false)]
public virtual IEnumerable sidePanelRefresh(PXAdapter adapter)
{
SOOrder doc = Base.Document.Current;
if (doc == null) return adapter.Get();
var docExt = doc.GetExtension<SOOrderExt>();
Dictionary<string, string> parameters = new Dictionary<string, string>();
string[] args = adapter.CommandArguments.Split('*');
switch (docExt.UsrSidePanelDetailParamsRefresh)
{
case ("Component Inventory"):
parameters.Add("Kit", args[0]);
parameters.Add("SiteID", args[1]);
throw new PXRedirectToGIRequiredException("Component Inventory", parameters) { Mode = PXBaseRedirectException.WindowMode.Layer };
}
return adapter.Get();
}
Note that for my use case the parameters I am passing cannot contain the character ‘*’, so I use it as a delimiter
And again, you can define as many case statements as you need for each of your dynamic side panels.
And now finally we need javascript again to call this action. Using event handlers are too slow for this type of refresh, they will always pass the old SOLine parameters. If you know a way to avoid javascript here, I’d love to hear it, as doing callbacks like this does have a heavy overhead. Because of this, be careful where you attach this script to, I actually attach it to the grid’s RowSelectorClick client event rather than AfterRowChange for this reason, and train my users to select rows in this way (by clicking the arrow to the left of each row) so we don’t impact the rest of the company for the minority that use this feature. I’d recommend this if you're working on a high traffic screen such as SO301000.
Anyway, here’s the javascript:
function Side_Panel_Refresh() {
px_alls["ds"].executeCallback("SidePanelRefresh", this.func.caller.arguments[2].row.getCell("InventoryID").getValue() + "*" + this.func.caller.arguments[2].row.getCell("SiteID").getValue());
}
SidePanelRefresh: Name of our graph action from the previous step.
InventoryID & SiteID are my parameters.
And again, using ‘*’ as a delimiter.
Anyway, I hope this helps anyone looking to make dynamic side panels. If anyone has improvements on this method I’d love to hear constructive feedback.
Happy development!
It is a new feature. Side panel were restricted to generic inquiry. In version 2020 you still need a generic inquiry to configure the side panel but they can be viewed in data entry forms too.
The feature is implemented with wizards so it's pretty limited in what it can do. Currently you are limited to the choices available in navigation parameter. It seems like it's not possible to add details entity there. Maybe there's a way but I'm not aware of it.
Check documentation reference link for User Interface: Side Panels for Data Entry Forms (content copied below):
https://help-2020r1.acumatica.com/Help?ScreenId=ShowWiki&pageid=a11aa71b-5f18-4c8f-9c69-350f2c7a89f4
Adding a Side Panel to the Data Entry Form
A customizer can add multiple side panel navigation paths to a data entry form.
To add a side panel, the customizer navigates to the Customization Projects (SM204505) form and creates a new project.
In the Customization Project Editor, the customizer selects the screen for which the side panel is planned to be added.
On the Screens > Form ID > Actions, the customizer adds an action of the Navigation:
Side panel type (1), specifies general settings (2), adds destination screen (3), and specifies parameters (4), if needed (for instance, in the screenshot below, Business Account was selected as a parameter so that the side panel displayed the cases of the specific customer selected on the data entry form).
Example of a side panel on the Customers form
To apply the changes, the customizer must publish the created customization project.
I am using a dynamic view panel along with a customizer bean (as demonstrated by Paul Calhoun on NotesIn9 back in episode 79 https://www.youtube.com/watch?v=AQOGgVZpAcw).
The customizer bean is currently very simple, just to ensure the documents are opened in read mode :-
public void afterCreateColumn(FacesContext context, int index,
ColumnDef colDef, IControl column) {
//Get a map of the session variables to read the view session scope variable
Map svals = context.getExternalContext().getSessionMap();
//Create a variable for the current component
UIComponent columnComponent = column.getComponent();
//Create a reference to the column and set the links to open in read mode
DynamicColumn dynamicColumn = (DynamicColumn) columnComponent;
//To have every view open the selected documents in read mode add the following
dynamicColumn.setOpenDocAsReadonly(true);
super.afterCreateColumn(context, index, colDef, column);
}
The dynamic view panel currently shows the first non-categorised column as a link to open the document. Can I add further customisation to show other columns as links, either instead of or as well as the first column?
Yes - you should be able to do this in a couple ways. The way I've done it in my customizer bean before is to handle it when fetching the column information at the front, since the goal of the bean is to match the view's design as closely as possible.
In your code there, though, you could also call dynamicColumn.setDisplayAs("link") or dynamicColumn.setDisplayAs("text") as appropriate to turn on or off link display.
I am trying to create an Entry page, and one of the options is to select an Item. the list can go more than a 1000 and it makes sense to show the search enabled page where the items are listed.
When the user clicks the "select item" from the Edit / create screen, I can pass the navigation parameter to that screen, and on selecting the item i can do a Frame.GoBack(). However, i cant pass any parameter back to the page. Is there a better way to do this?
At the moment I am thinking of using Global variables to store this data :(
Have you tried looking at the LayoutAwarePage.cs class in the Common folder and looking into the:
protected virtual void GoBack(object sender, RoutedEventArgs e)
{
if ((base.get_Frame() != null) && base.get_Frame().get_CanGoBack())
{
base.get_Frame().GoBack();
}
}
to see if you can pass an object?
I need to handle multiple panels, containing variuous data masks. Each panel shall be visible using a TreeView control.
At this time, I handle the panels visibility manually, by making the selected one visible and bring it on top.
Actually this is not much confortable, especially in the UI designer, since when I add a brand new panel I have to resize every panel and then design it...
A good solution would be using a TabControl, and each panel is contained in a TabPage. But I cannot find any way to hide the TabControl buttons, since I already have a TreeView for selecting items.
Another solution would be an ipotethic "StackPanelControl", where the Panels are arranged using a stack, but I couldn't find it anywhere.
What's the best solution to handle this kind of UI?
You need a wee bit of Win32 API magic. The tab control sends the TCM_ADJUSTRECT message to allow the app to adjust the tab size. Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form.
You'll get the tabs at design time so you can easily switch between pages. The tabs are hidden at runtime, use the SelectedIndex or SelectedTab property to switch between "views".
using System;
using System.Windows.Forms;
class StackPanel : TabControl {
protected override void WndProc(ref Message m) {
// Hide tabs by trapping the TCM_ADJUSTRECT message
if (m.Msg == 0x1328 && !DesignMode) m.Result = (IntPtr)1;
else base.WndProc(ref m);
}
}
A good solution would be using a TabControl, and each panel is contained in a TabPage.
But I cannot find any way to hide the TabControl buttons, since I already have a
TreeView for selecting items.
For the above,
You need to set the following properties of TabControl.
tabControl.Multiline = true;
tabControl.Appearance = TabAppearance.Buttons;
tabControl.ItemSize = new System.Drawing.Size(0, 1);
tabControl.SizeMode = TabSizeMode.Fixed;
tabControl.TabStop = false;
I have a user control in .NET 2010. I dragged it on to a page twice. Obviously, both have the same functions. But, depending on which instance was clicked, I want to run the function diffenetly. How can I tell which user control was clicked?
....
Let me add to this. It is a user control with a datalist in it. The data list contains numerous clickable images. When the image is clicked, I am trying to grab the name of the instance to use in the code of the user control itself.
Updated To Match Comment
The easiest way would be to use the sender parameter in your event handler. In your case, you should be able to cast to Control and then use the Parent property to get to the actual UserControl:
public void UserControlClickHandler(object sender, EventArgs e)
{
var senderAsControl = sender as Control;
var name = senderAsControl.Parent.Name;
}