Rozszerzalność istniejących widoków

Każdy istniejący w POS widok można rozszerzyć o dodatkowe elementy. Rozszerzenie może polegać na dodaniu nowej kolumny do datagrida, usunięciu istniejącej lub dodanie dowolnej kontrolki w miejscu do tego wcześniej przygotowanym. Możliwe jest również wywoływanie programów firm trzecich, poprzez proste dodanie kontrolki przycisku, pod którym będzie kryła się logika inicjująca inny proces.

Dodanie nowego elementu do istniejącego kontenera

Za pomocą rozszerzeń można dodać praktycznie dowolną kontrolkę do wybranego istniejącego widoku, ale tylko w miejscach uprzednio na to przygotowanych. Nie ma limitu liczby dodawanych elementów, jednakże można  je umieszczać tylko w specjalne przygotowanych na rozszerzalność kontenerach (ItemsContainer oraz Grid).

Aby dodać kontrolkę do istniejącego już widoku, należy zacząć od ustalenia czy widok jest zarządzalny i czy posiada odpowiedni kontener, w którym będzie można umieścić nowy element. W tym celu w aplikacji POS należy otworzyć widok zarządzania interfejsem. Z rozwijalnej drzewiastej listy widoków wybrać ten, dla którego będzie tworzone rozszerzenie. Następnie kliknąć w belkę Elementy, a w niej za pomocą rozwijalnej listy kontenerów wybrać, w którym miejscu tego widoku ma się znaleźć nowy element. Po wybraniu kontenera należy zapisać jego nazwę, ponieważ jest ona globalnym identyfikatorem, który będzie niezbędny na kolejnym etapie implementacji rozszerzenia.

Jeżeli tworząc własny widok chcemy umożliwić jego rozszerzalność, musimy przygotować na tę ewentualność miejsce, poprzez dodanie jednej bądź więcej kontrolek kontenera lub budując widok z użyciem kontrolki Grida (Comarch.POS.Presentation.Core.Controls), pamiętając o nadaniu im unikalnych identyfikatorów LayoutId (więcej o tym w artykule Zarządzanie widokiem i jego elementami).

Dodawanie kontrolki do widoku rozpoczynamy od utworzenia nowego modułu (patrz Nowy moduł) lub jeżeli już został utworzony przechodzimy do ciała metody Initialize() w klasie Module. Aby rozszerzyć widok o nową kontrolkę należy skorzystać z metody

AddButtonToContainer – w przypadku dodawania przycisku lub

AddElementToContainer<TFrameworkElement> – w przypadku dodawania dowolnej kontrolki typu FrameworkElement

Wymagane parametry obu metod to:

  • containerLayoutId (string) – identyfikator kontenera, do którego zostanie dodana kontrolka,
  • buttonLayoutId / elementLayoutId (string) – unikalny identyfikator nowej kontrolki (każda kontrolka musi posiadać niepowtarzalny identyfikator w kontenerze),
  • styleKey (string) – opcjonalna nazwa klucza w pliku ModernUI.xaml, gdzie zdefiniowany zostanie styl dla kontrolki,
  • buttonViewModelFunc / elementViewModelFunc (Func<IViewModel, FrameworkElementViewModel>) – opcjonalny parametr pozwalający na utworzenie lokalnego ViewModelu dla kontrolki. W ViewModel tym będzie można zdefiniować logikę, do której będzie mogła zbindować się kontrolka (za pomocą stylu).

 

Analogicznie, aby dodać element do Grida należy wywołać:

AddElementToGrid<TFrameworkElement> ­– parametry takie same jak dla elementów dodawanych do kontenera

 

Przykład.

Chcemy dodać przycisk do widoku dokumentu paragonu, którego kliknięcie spowoduje pojawienie się notyfikacji z wartością dokumentu.

Nazwa kontenera, do którego zostanie dodany przycisk to DocumentViewRightButtonsContainer. W klasie Module nowego modułu rozszerzającego w metodzie Initialize dodajemy linijkę:

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

gdzie ExtentionButton1 to unikalna nazwa (identyfikator LayoutId) dla nowego przycisku, ButtonStyle to nazwa dla klucza ze style dla tego przycisku, a ButtonViewModelFunc to metoda która będzie zwracać lokalny ViewModel w którym będzie zaimplementowana logika wywołująca powiadomienie z odpowiednią treścią.

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($"Wartość dokumentu: {_documentViewModel.Document.Value}", NotifyIcon.Information);                
    }
}

 

W pliku ModernUI.xaml naszego modułu dopisujemy styl dla nowego przycisku, określając w nim treść przycisku oraz bindujemy akcję kliknięcia do komendy powiązanej z metodą 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="Pokaż wartość" />
    <Setter Property="Command" Value="{Binding ExtensionButtonCommand}" />
</Style>

 

Pełny kod przykładu dostępny w Dodawanie kontrolki do kontenera istniejącego widoku

Dodanie kolumny do istniejącego DataGrida

Najprostszym sposobem dodania nowej kolumny do istniejącego datagrida widoku dokumentu (np. paragonu/faktury, zamówienia sprzedaży, itp.) jest przypisanie w systemie ERP atrybutu do elementu dokumentu (więcej w Obsługa atrybutów). Jeżeli natomiast nie chcemy, aby nowa kolumna była atrybutem, należy postąpić podobnie jak w przypadku dodawania kontrolek do kontenerów. W celu rozszerzenia istniejącej listy DataGrid musimy znać jej unikalny identyfikator. Aby go poznać, będąc w zarządzaniu widokami otwieramy widok zawierający datagrid, zaznaczamy go i odczytujemy jego Layout Id z sekcji Właściwości. Następnie za pomocą metody RegisterDataGridExtension znajdującej się w klasie ModuleBase, uzyskujemy dostęp do tej kontrolki i implementujemy dodanie nowej kolumny do kolekcji. Parametrami tej metody są:

  • dataGridLayoutId (string) – identyfikator layoutId rozszerzanego datagrida,
  • action (Action<DataGrid, IViewModel, bool>) – delegat do metody, która zostanie wywołana podczas tworzenia kontrolki datagrida

Przykład 1.

Chcemy dodać kolumnę do listy na widoku nowego paragonu, która będzie informować czy dodana pozycja nie przekracza zdefiniowanej kwoty.

Identyfikator Layout Id listy na paragonie to ReceiptDocumentViewDataGrid. W metodzie Initialize klasy Module dodajemy:

RegisterDataGridExtension("ReceiptDocumentViewDataGrid", DataGridNewColumn);

 

Następnie implementujemy metodę DataGridNewColumn:

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

 

Parametr isDesignMode przyjmie wartość true, gdy widok zawierający ten datagrid zostanie otwarty w trybie zarządzania interfejsem. Następnie dodajemy klasę konwertera ValidateConverter, która będzie zawierała logikę zwracającą odpowiednią wartość dla każdej komórki dodanej kolumny:

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 ? "TAK" : "NIE";
        }
 
        //w przypadku pozycji na skup
        return "Nie dotyczy";
    }
   … 
}

 

Powyższy przykład zakłada, że wszystkie informacje niezbędne do zaprezentowania wartości są dostępne w encji wiersza. W przypadku, gdy logika biznesowa dla nowej kolumny też musi zostać rozszerzona należy najpierw pobrać niezbędne dodatkowe dane dla nowej kolumny. Pobrane dane możemy przechować w specjalnie przygotowanej publicznej właściwości dostępnej na każdym viewmodelu – CustomDataDictionary. Jest to właściwość typu słownikowego (string, object), do której możemy się odwołać w zdefiniowanej kolumnie za pomocą bindingu.

 

Przykład 2.

Dodajemy nową kolumnę na widoku paragonu, która będzie prezentować nazwę cennika produktu dodanego na listę. Encja produktu (IDocumentItemRow) zawiera tylko identyfikator cennika (PriceListId), ale nie posiada jej nazwy.

 

Zaczynamy od pobrania pełnej listy cenników i zapisania jej w słowniku CustomDataDictionary. Cenniki wystarczy, że pobierzemy tylko raz na początku, np. podczas otwierania widoku. W tym celu możemy skorzystać z extension points i wpiąć się w AfterOnInitializationEvent, który jest wywoływany z metody AfterOnInitialization po inicjalizacji, lub poprzez podziedziczenie po klasie DocumentViewModel i przeciążenie metody OnInitialization. Możemy to również zrobić podczas wpinania się z nową kolumną, czyli w akcji metody RegisterDataGridExtension. Na potrzeby tego przykładu wybierzmy właśnie tę ostatnią metodę.

W metodzie Initialize klasy Module wywołujemy:

RegisterDataGridExtension("ReceiptDocumentViewDataGrid", DataGridNewColumnWithCustomBL);

 

Następnie implementujemy metodę DataGridNewColumnWithCustomBL

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, "pierwszy" },
                    {2, "drugi" }
                });
 
                //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);
}

 

W pierwszej części metody symulujemy pobranie cenników poprzez wypełnienie słownika CustomDataDictionary słownikiem (id cennika, nazwa cennika) z dwiema wartościami. Celowo tworzymy słownik w słowniku, ponieważ CustomDataDictionary może potencjalnie przydać się to przechowywania również innych informacji (np. dla innej logiki biznesowej). Klucz CustomColumnTest to zdefiniowane w klasie Module pole typu const string zawierające unikalną nazwę, po której będziemy identyfikować naszą kolekcję danych (cenniki).

public const string CustomColumnTest = "CustomColumnTest";

W drugiej części metody tworzona jest kolumna z multibindingiem wraz z konwerterem. Multibinding definiuje binding do aktualnej encji wiersza oraz do słownika z cennikami zawartego w słowniku CustomDataDictionary pod kluczem CustomColumnTest. W konwerterze natomiast uzyskujemy oba obiekty, dzięki czemu możemy zwrócić nazwę cennika na podstawie id zawartego w encji oraz nazwy zawartej w słowniku.

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;
        }
... 
    }

 

Pełny przykład zawiera jeszcze wpięcie na metodę SetInitialPrice, w którym wywoływane jest żądanie odświeżenia bindingu z CustomDataDictionary, ponieważ po zmianie ceny początkowej prezentowana cena nie jest już ceną z cennika (właściwość PriceListId  będzie teraz nullem) i nowa kolumna z nazwą nie powinna jej już prezentować.

 

Pełny kod przykładów dostępny w Dodawanie kolumny do DataGrida na istniejącym widoku

Dostęp do istniejącego elementu

Możliwe jest również uzyskanie dostępu do właściwości każdej istniejącej kontrolki, która posiada ustawiony layoutId. W celu w klasie Module należy skorzystać z metody AttachToFrameworkElement. Metoda posiada analogiczne parametry jak metoda RegisterDataGridExtension.

Dodawanie elementów do obszaru statusowego

Obszar statusowy charakteryzuje się tym, że elementy tam umieszczone dostępne są przez cały czas działania aplikacji, niezależnie od otwartych widoków. Dostęp do tego obszaru możliwy jest z poziomu dowolnego widoku podstawowego. W celu dodania kontrolki z własną logiką do obszaru statusowego należy w klasie Module, w metodzie Initialize wywołać metodę AddElementToStatusBar<TFrameworkElement>. Argumentami metody są:

  • elementLayoutId (string) – unikalny identyfikator dodawanej kontrolki,
  • styleKey (string) – nazwa klucza w pliku ModernUI.xaml, gdzie zdefiniowany zostanie styl dla kontrolki,
  • elementViewModelFunc (Func<IStatusBar,StatusBarEementBase>) – delegat do metody, która zostanie wywołana podczas tworzenia kontrolki

Przykład implementacji w Przykład rozszerzenia obszaru statusowego

Czy ten artykuł był pomocny?