[MSCE U16 C#, C++] Create custom PickListProvider for PickList values

In Update 16, we have introduced PickList Source and Settings for extracting PickList values from different sources.
You can access this from:

ItemType Manager -> New Property -> PickList Options

1. In the PickList Source drop-down, select from:
        Dgn Files (*.dgn, *.dgnlib
        Excel (*.xlsx)

2. Select Settings

Based on your selection the respective UI will display for Dgn Files or Excel. Refer images below:
By choosing the appropriate option you can configure how the PickList values can be populated in PickList Values.



In Update 16 we have introduced a way to add custom PickList Source and corresponding UI.
SDK Sample:

In this blog, we explain:

1. How to introduce the custom PickList provider and UI using managed APIs.
    This is available at: ..\examples\DgnEC\PickListProviderExample\ManagedExample

2. how to introduce the custom PickList provider using native API’s and UI using managed APIs.
    This is available at: ..\examples\DgnEC\PickListProviderExample\NativeExample

Implementation: 

Step 1: Implement IPickListNetProvider:


Managed:
IPickListNetProvider is an abstract class, available under Bentley.DgnPlatformNET namespace in Bentley.DgnPlatformNET.dll.
In the class, the constructer sets the unique value of the provider and display label.
Implement GetStandardValues() and IsValidSettings().
GetStandardValues(): Add custom implementation using which the data will be extracted from the new source introduced.
IsValidSettings(): Check provided settings are valid or not by adding custom checks specific to a new provider.
A sample implementation is:

class PickListCSVNetProvider : IPickListNetProvider
{
//--------------------------------------------------------------------------------------
// @description   Constrcutor for PickListCSVNetProvider
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------

public PickListCSVNetProvider() : base ("CSVFileManaged", "CSVFileManaged (*.csv)")
    {
    }

//--------------------------------------------------------------------------------------
// @description   Get the PickList Values from provider.
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public override IList<string> GetStandardValues(DgnFile dgnFile, IECProperty ecProperty, IECPrimitiveType primitiveType)
    {
    List <string> values = new List<string>();
    //validate provider
    if (!IPickListNetProvider.GetProviderName(dgnFile, ecProperty).Equals(this.Name))
        return values;

    string providerSettings = IPickListNetProvider.GetProviderSettings(dgnFile, ecProperty);

    if (!IsValidSettings(providerSettings))
        return values;

    return ReadCSVData(providerSettings); 
    }

//--------------------------------------------------------------------------------------
// @description   Check settings are valid or not 
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public override bool IsValidSettings(string providerSettings)
    {
    //Check file exists and file extension 
    if (string.IsNullOrEmpty(providerSettings))
        return false;

    if (!File.Exists(providerSettings) && !Path.GetExtension(providerSettings).Equals(".csv"))
        return false;

    return true;
    }
    
//--------------------------------------------------------------------------------------
// @description   Read values from CSV file. 
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
private List<string> ReadCSVData(string filePath)
    {
    List<string> fileData = new List<string>();
    try
        {
        using (var reader = new StreamReader(filePath))
            {
            while (!reader.EndOfStream)
                {
                var line = reader.ReadLine();
                string[] values = line.Split(',');
                if(null != values && values.Length > 0 )
                    {
                    foreach (var value in values)
                        fileData.Add(value.Trim('"'));
                    }
                }
            }
        }
    catch(Exception ex)
        {
        throw new Exception($"Error reading CSV file {filePath}", ex.InnerException);
        }
    return fileData;
    }
}


Native:
IPickListProvider is an abstract class, available under Bentley::DgnPlatform namespace in Bentley.DgnPlatform5.dll.
In the class, the constructer sets the unique value of the provider and display label.
Implement GetStandardValues() and IsValidSettings().
GetStandardValues(): Add custom implementation using which the data will be extracted from the new source introduced.
IsValidSettings(): Check provided settings are valid or not by adding custom checks specific to a new provider.
A sample implementation is:
struct PickListCSVProvider : Bentley::DgnPlatform::IPickListProvider
{
public:
    PickListCSVProvider();
    virtual bool                GetStandardValues(DgnFileP dgnFile, ECN::ECPropertyCP ecProperty, ECN::PrimitiveType primitiveType, StandardValuesCollection& values) override;
    virtual bool                IsValidSettings(WCharCP settings) const override;
private:
    static const WString        ProviderName;   
    bool                        GetCSVFileData(ECN::ECValue ecValueProviderSetting, StandardValuesCollection& values);
};

/*---------------------------------------------------------------------------------**//**
*  PickListCSVProvider constructer
* @bsimethod                                                            Bentley Systems
+---------------+---------------+---------------+---------------+---------------+------*/
PickListCSVProvider::PickListCSVProvider() : Bentley::DgnPlatform::IPickListProvider(PickListCSVProvider::ProviderName.c_str())
    {
    SetDisplayLabel(L"CSVFileNative (*.csv)");
    }

/*---------------------------------------------------------------------------------**//**
*  This function has CSV provider specifc picklist value extraction code. 
* @bsimethod                                                            Bentley Systems
+---------------+---------------+---------------+---------------+---------------+------*/
bool  PickListCSVProvider::GetStandardValues(DgnFileP dgnFile, ECN::ECPropertyCP ecProperty, ECN::PrimitiveType primitiveType, StandardValuesCollection& values)
    {
    ECN::ECValue ecValueProviderName;
    bool status = IPickListProvider::GetProviderName(ecProperty, ecValueProviderName);
    if (!status || ecValueProviderName.IsNull() || !PickListCSVProvider::ProviderName.Equals(ecValueProviderName.GetString()))
        return false;
    
    ECN::ECValue ecValueProviderSetting;
    status = IPickListProvider::GetProviderSettings(ecProperty, ecValueProviderSetting);
    if (!status || ecValueProviderSetting.IsNull() || WString::IsNullOrEmpty(ecValueProviderSetting.GetString()))
        return false;

    GetCSVFileData(ecValueProviderSetting, values);

    return true;
    }

/*---------------------------------------------------------------------------------**//**
* Check picklist settings are valid or not for CSV Provider.
* @bsimethod                                                            Bentley Systems
+---------------+---------------+---------------+---------------+---------------+------*/
bool  PickListCSVProvider::IsValidSettings(WCharCP settings) const
    {
    return true;
    }

/*---------------------------------------------------------------------------------**//**
* Get the actual data by reading csv file
* @bsimethod                                                            Bentley Systems
+---------------+---------------+---------------+---------------+---------------+------*/
bool PickListCSVProvider::GetCSVFileData(ECN::ECValue ecValueProviderSetting, StandardValuesCollection& values)
    {
    std::wifstream fileStream(ecValueProviderSetting.GetString());
    std::wstring                cell;

    while (std::getline(fileStream, cell, L','))
        {
        cell.erase(remove(cell.begin(), cell.end(), '\"'), cell.end());
        values.push_back(cell.c_str());
        }

    return true;
    }


Step 2: Register Provider:
Register provider implementation:

Managed:
//Initialize provider object and register 
    PickListCSVNetProvider pickListCSVNetProvider = new PickListCSVNetProvider();
    bool status = Bentley.DgnPlatformNET.IPickListNetProvider.RegisterProvider(pickListCSVNetProvider);

Native:
/*---------------------------------------------------------------------------------**//**
* Static Member Initialization
+---------------+---------------+---------------+---------------+---------------+------*/
static PickListCSVProvider* s_csvPickListProvider = NULL;

/*---------------------------------------------------------------------------------**//**
* This function is called on key-in: PickListSettingsNativeExample  InitializeProvider
* @bsimethod                                                            Bentley Systems
+---------------+---------------+---------------+---------------+---------------+------*/
void InitializeProvider()
    {
    s_csvPickListProvider = new PickListCSVProvider();
    if (SUCCESS == Bentley::DgnPlatform::DgnECManager::GetManager ().RegisterPickListProvider (s_csvPickListProvider))
        mdlOutput_messageCenter (OutputMessagePriority::Info, L"Initialized native PickListCSVProvider", NULL, OutputMessageAlert::None);
    else
        mdlOutput_messageCenter (OutputMessagePriority::Info, L"Failed to initializ native PickListCSVProvider", NULL, OutputMessageAlert::None);
    }


Step 3: Create Custom User control by implementing IPickListDataSettings:
Note: UI will be always in Managed code.
IPickListDataSettings is an interface available in Bentley.DgnPlatformNET namespace in Bentley.DgnDisplayNet.dll.
Implement IPickListDataSettings in ViewModel class created for User control:

This function is used to add conditions to enable/disable UI with an appropriate error message.

   bool CanShowUIForProperty(CustomProperty customProperty, ITypeDescriptorContext context, out string errorMessage);  

This function will return the string output to be stored in the Item type property.

   string GetPickListSetting();

UserControl GetUI(CustomProperty customProperty, ITypeDescriptorContext context);

This is a custom user control that will be shown for extracting data from the new PickList Provider.
Custom control implementation of xaml : 
<UserControl x:Class="PickListProviderManagedExample.PickListCSVProvider"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>

        <TextBox x:Name="TxtCSVPath"
                 Grid.Row="0"
                 Grid.Column="0"
                 HorizontalAlignment="Stretch"
                 Margin="2"
                 Text="{Binding CSVFilePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        </TextBox>
        <Button x:Name="BtnSelect"
                Content="Select CSV File"
                MinWidth="100"
                Height="25"
                Grid.Column="1"
                Grid.Row="0"
                HorizontalAlignment="Left"
                Margin="2"
                Command="{Binding BrowseCSVFileCommand}">
        </Button>
    </Grid>
</UserControl>


Implementation of xaml.cs:
public partial class PickListCSVProvider : UserControl
{
internal PickListCSVProviderVm ViewModel { get; set; }

/*---------------------------------------------------------------------------------**//**
* Constructor for PickListCSVProvider
* @bsimethod                                                              Bentley Systems
/*--------------+---------------+---------------+---------------+---------------+------*/
public PickListCSVProvider()
    {
    InitializeComponent();
    ViewModel = new PickListCSVProviderVm(this);
    DataContext = ViewModel;
    }
}
 

Implementation of ViewModel:
internal class PickListCSVProviderVm : Bentley.UI.Mvvm.ViewModelBase, IPickListDataSettings
{
private DependencyObject m_dependencyObject;
private string           m_CSVFilePath;
public string CSVFilePath 
    {
    set 
        {
        m_CSVFilePath = value;
        OnPropertyChanged("CSVFilePath");
        }
    get { return m_CSVFilePath;}
    }

public ICommand BrowseCSVFileCommand { get; private set; }

//--------------------------------------------------------------------------------------
// @description   Constrcutor for PickListCSVProvider
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public PickListCSVProviderVm(DependencyObject pickListCSVProvider)
    {
    m_dependencyObject = pickListCSVProvider;
    BrowseCSVFileCommand = new RelayCommand(OnBrowseCSVCick);
    }

//--------------------------------------------------------------------------------------
// @description   Hanlder for Browse button click 
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
private void OnBrowseCSVCick()
    {
    OpenFileDialog csvFileDialog = new OpenFileDialog();
    csvFileDialog.Filter = "CSV Files (*.csv)|*.csv";
    csvFileDialog.FilterIndex = 1;
    csvFileDialog.Multiselect = false;

    if (csvFileDialog.ShowDialog() == DialogResult.OK)
        {
        CSVFilePath = csvFileDialog.FileName;
        }
    }

//--------------------------------------------------------------------------------------
// @description   Conditon to enable/disable UI with apprpriate error message.
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public bool CanShowUIForProperty(CustomProperty customProperty, ITypeDescriptorContext context, out string errorMessage)
    {
    errorMessage = string.Empty;
    return true;
    }

//--------------------------------------------------------------------------------------
// @description   Send the string to store as PickList Setting
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public string GetPickListSetting()
    {
    return CSVFilePath;
    }

//--------------------------------------------------------------------------------------
// @description   Send the UserControl used for this provider
// @bsimethod                                                    Bentley
//+---------------+---------------+---------------+---------------+---------------+------
public System.Windows.Controls.UserControl GetUI(CustomProperty customProperty, ITypeDescriptorContext context)
    {
    string pickListSettingVal = customProperty.PickListSettings;
    CSVFilePath = string.IsNullOrEmpty(pickListSettingVal) ? string.Empty : pickListSettingVal;
       
    return (System.Windows.Controls.UserControl)m_dependencyObject;
    }
}


4. Register UI class:
//Initialize UI object and register 
IPickListDataSettings csvUI = new PickListCSVProvider().ViewModel;
PickListUIManager.Instance.RegisterUI(pickListUIName, csvUI);


5. See it running:
With this sample app you must execute the following key-ins:

Managed:
mdl load PickListProviderManagedExample

picklistprovidermanagedexample initializeprovider 				:    This keyin will register managed PickList provider. 
picklistprovidermanagedexample initializeui CSVFileManaged      :    This keyin will register UI component for managed PickList provider.

When you create a new property the PickList Source drop-down will show a new entry for selecting CSV files.
Click on “CSVFileManaged (*.csv)“.
Select the settings option and you will see :

Browse CSV file provided as sample data with the example code. This browsed file will be used for PickList values. e.g. \DgnEC\PickListProviderExample\ManagedExample\SampleData.csv
Save the property.
Go to the Attach item dialog and select the Property dropdown. You will see the entries from CSV file.


Native:
mdl load PickListProviderNativeExample

PickListProviderNativeExample InitializeProvider               :    This keyin will register managed PickList provider.

For registering UI component use managed sample app:
mdl load PickListProviderManagedExample

picklistprovidermanagedexample initializeui CSVFileNative       :    This keyin will register UI component for native PickList provider.

When you create a new property the PickList Source drop-down will show a new entry for selecting CSV files.
Click on “CSVFileNative (*.csv)“.
Select the settings option and you will see :

Browse CSV file provided as sample data with the example code. This browsed file will be used for PickList values. e.g. \DgnEC\PickListProviderExample\NativeExample\SampleData.csv
Save the property.
Go to the Attach item dialog and select the Property dropdown. You will see the entries from CSV file.

Note: SDK sample should be available in the U16 release.