Extensibility of existing views

Each view existing in POS application can be extended with additional elements. Such an extension may involve adding a new column to a datagrid, deleting an existing control, or adding a new control in a pre-designed place for it. You can also call third-party programs by simply adding a button control whose logic would initiate another process.

How to add a new element to an existing container

Using extensions, you can add virtually any control to a selected existing view, provided that you do it in a pre-designed location. The number of elements that can be added is unlimited; however, they can only be placed inside containers (ItemsContainer and Grid) prepared specifically for the purposes of extensibility.

To add a control within an existing view, begin from determining whether the view is manageable and whether it has an appropriate container where the new element could be placed. To do so, open the layout management view in POS application. From the drop-down list of views, select the one for which you want to create an extension. Next, select the Elements bar and, using the displayed drop-down list of containers, indicate where the new element should be located within the selected view. Once you choose a container, save its name, as it is a global identifier that will be necessary at the next stage of extension implementation.

If a view you are creating should be extensible, an appropriate area should be pre-designed for this eventuality by adding one or more container controls or by building the view using a Grid control (Comarch.POS.Presentation.Core.Controls); note that the controls should be assigned unique LayoutId identifiers (see How to manage the layout and its elements for more details).

To add a control to the view, start by creating a new module (see the chapter New module in How to create views) or, if it has already been created, go to the body of the Initialize() method in the Module class. To extend the view with a new control, use one of the following methods:

  • AddButtonToContainer – when adding a button
  • AddElementToContainer<TFrameworkElement> – when adding any control of the FrameworkElement type

Both the methods require the following parameters:

  • containerLayoutId (string) – it is the identifier of the container to which the control is to be added
  • buttonLayoutId / elementLayoutId (string) – it is the unique identifier of the new control (each control must be assigned a unique identifier within the container)
  • styleKey (string) – it is an optional key name in the ModernUI.xaml file, where the control’s style will be defined
  • buttonViewModelFunc / elementViewModelFunc (Func<IViewModel, FrameworkElementViewModel>) – it is an optional parameter allowing you to create a local ViewModel for the control. In this ViewModel, you will be able to define the logic that the control can bind to (using a style)

Similarly, to add an element to a Grid, call the following:

  • AddElementToGrid<TFrameworkElement> – the same parameters as for elements added to the container
Example

You want to add a new button to the receipt document view; clicking the button should open a notification presenting the document value.

The name of the container to which the button will be added is DocumentViewRightButtonsContainer. In the Module class of the new extension module, add the following line within the Initialize method:

AddButtonToContainer("DocumentViewRightButtonsContainer", "ExtensionButton1", "ButtonStyle", ButtonViewModelFunc);

where ExtensionButton1 is the unique name (LayoutId identifier) of the new button, ButtonStyle is the name of the style key of the button, and ButtonViewModelFunc is a method that will return the local ViewModel, where the logic opening the relevant notification will be implemented.

private FrameworkElementViewModel ButtonViewModelFunc(IViewModel viewModel)
{
    return new ButtonViewModel(viewModel, ViewManager, Container);
}
 
public class ButtonViewModel : FrameworkElementViewModel
{
    public DelegateCommand ExtensionButtonCommand { get; set; }
 
    private readonly IDocumentViewModel _documentViewModel;
    private readonly INotificationService _notifyService;
 
    public ButtonViewModel(IViewModel viewModel, IViewManager viewManager, IUnityContainer container) : base(viewModel, viewManager)
    {
        if (viewModel.IsDesignMode)
            return;

        _notifyService = container.Resolve<INotificationService>();
        _documentViewModel = (DocumentViewModel)viewModel;
        ExtensionButtonCommand=new DelegateCommand(ExtensionButtonAction);
    }
 
    private void ExtensionButtonAction()
    {
        _notifyService.Show($”Document value: {_documentViewModel.Document.Value}", NotifyIcon.Information);                
    }
}

In the ModernUI.xaml file of your module, add the style of the new button, specifying the button’s text within the style. Next, bind the click action to the command associated with the ExtensionButtonAction.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:buttons="clr-namespace:Comarch.POS.Presentation.Core.Controls.Buttons;assembly=Comarch.POS.Presentation.Core">

<Style x:Key="ButtonStyle" TargetType="buttons:Button"
       BasedOn="{StaticResource {x:Type buttons:Button}}">
    <Setter Property="Content" Value=”Show Value" />
    <Setter Property="Command" Value="{Binding ExtensionButtonCommand}" />
</Style>

See How to add a control to the container of an existing view for the complete code of the example.

How to add a column to an existing DataGrid

The simplest way to add a new column to an existing DataGrid of a document view (e.g. a receipt/invoice, sales order, etc.), is to assign an attribute to a document element in the ERP system (see the chapter How to handle attributes in Layout management extensibility for more details). However, if you do not want the new column to be attribute-related, you need to complete similar steps as when adding controls to containers. To extend an existing DataGrid list, you must find its unique identifier. To do so, open the layout management mode and select the view with the DataGrid; next, select the DataGrid and read its LayoutId in the Properties section. In the next step, with the help of the RegisterDataGridExtension method contained in the ModuleBase class, you can gain access to the control and implement the new column within the collection. The method’s parameters are:

  • dataGridLayoutId (string) – it is the LayoutId identifier of the DataGrid to be extended
  • action (Action<DataGrid, IViewModel, bool>) – it is the delegate for the method that will be called when creating the DataGrid control
Example

You want to add a column to the list in the new receipt view that would show whether a given document item exceeds a defined amount.

The LayoutId identifier of the receipt’s list is ReceiptDocumentViewDataGrid. In the Initialize method of the Module class, add the following:

RegisterDataGridExtension("ReceiptDocumentViewDataGrid", DataGridNewColumn);

Next, implement the DataGridNewColumn method:

private void DataGridNewColumn(DataGrid dataGrid, IViewModel viewModel, bool isDesignMode)
{
    var column = new DataGridTextColumn
    {
        Header = “Exceeds 100?",
        Binding = new Binding {Converter = new ValidateConverter()}
    };
 
    Layout.SetId(column, "DocumentViewDataGridExtendedColumn1"); 
    dataGrid.Columns.Add(column);
}

The isDesignMode will have the value true if the view containing this DataGrid is opened in the layout management mode. Next, add the converter class ValidateConverter that is to contain the logic returning a relevant value for each cell of the newly added column:

internal class ValidateConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var row = value as TradeDocumentItemRow;
 
        if (row!=null)
        {
            return row.Price > 100 ? “YES" : “NO";
        }
 
        //in the case of buy-back items
        return “Not applicable";
    }
   … 
}

The example above assumes that all information needed to present the value is available in the row’s entity. If the business logic of the new column should also be extended, it is first necessary to get required additional data for the new column. You can store the retrieved data in a specially prepared public property available within each viewmodelCustomDataDictionary. It is a property of the dictionary type (string, object) that you can refer to in the defined column with the use of binding.

Example

You want to add a new column to the receipt view that would show the name of the price list of an item added on the receipt’s item list. The item entity (IDocumentItemRow) contains only the price list identifier (PriceListId), but does not contain the price list’s name.

Start by downloading the complete list of price lists and saving it in CustomDataDictionary. It is enough to download the price lists once, for instance, when opening the view. To do so, you may use extension points and attach to AfterOnInitializationEvent which is invoked from the AfterOnInitialization method upon initialization or inherit from the DocumentViewModel class and overload the OnInitialization method. You can also do so when attaching a new column, that is in the action of the RegisterDataGridExtension method. For the purposes of this example, the latter method will be selected.

In the Initialize method of the Module class, invoke the following:

RegisterDataGridExtension("ReceiptDocumentViewDataGrid", DataGridNewColumnWithCustomBL);

Next, implement the DataGridNewColumnWithCustomBL method:

private void DataGridNewColumnWithCustomBL(DataGrid dataGrid, IViewModel viewModel, bool isDesignMode)
{
       if (viewModel is CustomDocumentViewModel vm)
       {
                //fill custom dictionary with dictionary of data for custom column (priceListId => name)
                vm.CustomDataDictionary.Add(CustomColumnTest, new Dictionary<int, string>
                {
                    {1, “first" },
                    {2, “second" }
                });
 
                //after initial price changed refresh custom column binding
                vm.AfterSetInitialPrice += () => { 
                       vm.OnPropertyChanged(nameof(vm.CustomDataDictionary)); 
                };
       }
 
       var column = new DataGridTextColumn
       {
          Header = "Price list name",
          Binding = new MultiBinding
          {
            Converter = new CustomMultiConverter(),
            Bindings =
            {
              new Binding(),
              new Binding
              {
                RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DocumentView), 1),
 Path = new PropertyPath($"DocumentViewModel.CustomDataDictionary[{CustomColumnTest}]")
              }
            }
          }
       };
 
    dataGrid.Columns.Add(column);
}

In the first part of the method, we simulate getting the price lists by completing CustomDataDictionary with a dictionary (price list id, price list name) containing two values. We create a dictionary within a dictionary on purpose, since CustomDataDictionary may be potentially useful for storing other information as well (for instance, for another business logic). The CustomColumnTest key is a const string field defined in the Module class, containing the unique name by which we will identify our data collection (price lists).

public const string CustomColumnTest = "CustomColumnTest";

The second part of the method creates the column with multibinding along with a converter. The multibinding defines the binding to the current row entity and to the dictionary with the price lists contained in CustomDataDictionary under the CustomColumnTest key. In turn, the converter gets both the objects, thanks to which we can return a price list’s name based on an id contained within the entity and a name contained in the dictionary.

internal class CustomMultiConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var documentItemRow = values[0] as IDocumentItemRow;
            var dictionary = values[1] as Dictionary<int, string>;
 
            //if item has price list id then show price list name (when initial price changes, price list id nulls)
            if (documentItemRow?.PriceListId.HasValue ?? false)
            {
                string name = null;
                if (dictionary?.TryGetValue(documentItemRow.PriceListId.Value, out name) ?? false)
                {
                    return name;
                }
            }
 
            return null;
        }
... 
    }

The full example also contains an attachment to the SetInitialPrice method which invokes a request to refresh the binding from CustomDataDictionary, since, after changing the regular price, the displayed price no longer originates from a price list (the PriceListId property will be null) and the new column with the name should not present it anymore.

See the chapter How to add a column to the DataGrid of an existing view in Examples for the complete code of the examples.

Access to an existing element

It is also possible to gain access to the properties of each existing control that has a defined layoutId. To do so, it is necessary to use the AttachToFrameworkElement method in the Module class. The method has the same parameters as RegisterDataGridExtension.

How to add elements to the status bar

What characterizes the status bar is that elements placed on it are available for the whole time the application is running, regardless of the open views. The bar can be accessed in any primary view. In order to add a control with own logic to the status bar, invoke the AddElementToStatusBar<TFrameworkElement> method in the Initialize method of the Module class. The method’s arguments are:

  • elementLayoutId (string) – it is the unique identifier of the control to be added
  • styleKey (string) – it is a key name in the ModernUI.xaml file, where the control’s style will be defined
  • elementViewModelFunc (Func<IStatusBar,StatusBarElementBase>) – it is the delegate for the method that will be called when creating the control

See the chapter How to extend the status bar in Examples to see an implementation example.

Czy ten artykuł był pomocny?