Select to view content in your preferred language

WPF: cannot fill more than one combo boxes with domain values

2404
8
Jump to solution
07-21-2020 07:48 AM
BarbaraSchneider2
Frequent Contributor

I have a ProWindow with two combo boxes on it. The combo box items are retrieved from coded value domains. I followed the guidelines for thread safe data binding with WPF on ProConcepts Framework · Esri/arcgis-pro-sdk Wiki · GitHub (section Thread safe data binding with WPF).

The data binding is as follows:

public ReadOnlyObservableCollection<string> StreetItems
{
    get { return _streetItems; }
}
public ReadOnlyObservableCollection<string> RiverItems
{
   get { return _riverItems; }
}

The code for filling the first combo box is as follows (the second combo box is filled accordingly):

var items = new ObservableCollection<string>();
_streetItems= new ReadOnlyObservableCollection<string>(items);
object lockObject = new object();
BindingOperations.EnableCollectionSynchronization(_streetItems, lockObject);
var codedValuePairs = await GetCodedValuePairsFromDomainAsync(layerName, fieldName);
               
lock (lockObject)
{
   foreach (var keyValuePair in codedValuePairs)
   {
      items.Add(keyValuePair.Value);
   }
}  

The problem is that I can only fill one combo box with the domain values. The other combo box (whether it is the first or the second) always remains empty.  What am I doing wrong?

0 Kudos
1 Solution

Accepted Solutions
BarbaraSchneider2
Frequent Contributor

I found out what the problem is. In the constructor of my view model, I call the two functions to fill my combo boxes:

await FillStreetComboboxAsync();
await FillRiverComboboxAsync();

If I remove the "await", my two combo boxes get filled.

FillStreetComboboxAsync();
FillRiverComboboxAsync();

Here is the method FillStreetComboboxAsync():

private async void FillStreetComboboxAsync()
{          
    var items = new ObservableCollection<string>();
    _streetItems = new ReadOnlyObservableCollection<string>(items);

    object lockObject = new object(); 
    BindingOperations.EnableCollectionSynchronization(_streetItems , lockObject);
    var codeValuePairs = await GetCodeValuePairsFromDomainAsync(_layerName, _fieldName);     

    foreach (var keyValuePair in codeValuePairs)
    {
        items.Add(keyValuePair.Value);
    }                                                               
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Method fill RiverComboboxAsync() is analogous.

View solution in original post

0 Kudos
8 Replies
by Anonymous User
Not applicable

Hi Barbara Schneider‌,

That thread safe data binding with that code example mention to use lock because it is running under the MCT thread

 public void FillCollectionAsync()
  {

    QueuedTask.Run(() => // this is mct thread
    {
      // Reads and Writes should be made from within the lock
      lock (_lockObj)
      {
         _items.Add( GetData() );
      }
//don't perform any UI update inside
//you can update the variable but don't send notify property change command to UI to execute UI thread
    });

//you can perform UI thread/ui update here inclsive of binded collection
  }

For your case seem to be outside of UI thread, so you might not need Lock keyword at all.

Additionally it could be simpler to use MVVM pattern done by ArcGIS pro.

For prowindows case you can achieve this by defining datacontext with vm

 public partial class sampleprowindow: ArcGIS.Desktop.Framework.Controls.ProWindow
    {
        public sampleprowindowVm = new sampleprowindowVm (); // new class define
        public sampleprowindow()
        {
            InitializeComponent();
//load the vm properties here
            this.DataContext = Vm;
        }
    }
BarbaraSchneider2
Frequent Contributor

Hello Than Aung,

thank you for your reply! I already use the MVVM pattern using a ViewModel class. Yes, you are right, adding items to the combo box is not inside the MCT thread, so I removed the following code:

lock (lockObject)
{
   ...
}

However, I need the following line of code because getting coded value pairs from domain is done inside the MCT thread:

BindingOperations.EnableCollectionSynchronization(_streetItems, lockObject);

Otherwise, the combo box remains empty.

I still have the problem that I can only fill one combo box with the domain values. The other combo box (whether it is the first or the second) always remains empty.

0 Kudos
NarelleChedzey
Esri Contributor

Barbara, 

I am assuming that when you debug your code the values in your 'codedValuePairs' variable for the second domain is empty?.   What happens when you debug into your GetCodedValuePairsFromDomainAsync method.  Is the second domain found successfully?     Can you post the code in this method. 

Thanks

Narelle

0 Kudos
BarbaraSchneider2
Frequent Contributor

Narelle,

no, the values in my 'codedValuePairs' variable for the second domain are not empty. Yes, the second domain is found successfully. Please see below for the entire code I use.

Thanks, Barbara

0 Kudos
by Anonymous User
Not applicable

Hi Barbara Schneider‌,

I got same situation like this before with MCT thread in MVVM binding,

We shall need to use temporary local variable and assign back to binding variable after the MCT thread will help you.

There is another way using await variable return the value and accept at the binding variable.

Below is the sample snippets of two solutions.

That how I normally solve, 

I used solution 1, If I need to rebind or notify UI update back with multiple properties/variable., if only one, I use solution 2.

//solution 1
public async void BindingOperationAsync()
  {
var tmpBindingVariable = null; // or define the same type as binding variable
   await QueuedTask.Run(() => // this is mct thread
    {
      //perform your mct operation 
tmpBindingVariable = ....... /// assign the result to tmpBindingVariable
  });

this.bindingVariable = tmpBindingvariable;
}


//solution 2
 public async void BindingOperationAsync()
  {

   this.bindingVariable = await QueuedTask.Run(() => // this is mct thread
    {
      //perform your mct operation 
return value; //same type as this.bindingVariable
  });
}
BarbaraSchneider2
Frequent Contributor

Hello Than,

I had already tried the solution with the temporary variable, but this doesn't work. I think that I already use the second solution, but I am not sure. Here is my code. There is probably something wrong with it:

var items = new ObservableCollection<string>();
_streetItems= new ReadOnlyObservableCollection<string>(items);

object lockObject = new object();
BindingOperations.EnableCollectionSynchronization(_streetItems, lockObject);

var codedValuePairs = await GetCodedValuePairsFromDomainAsync(layerName, fieldName);           
foreach (var keyValuePair in codedValuePairs)
{
  items.Add(keyValuePair.Value);
}

public static async Task<SortedList<object, string>> GetCodeValuePairsFromDomainAsync(string featLayerName, string fieldName)
{                   
    Domain domain = await GetDomainByFeatureLayerAsync(featLayerName, fieldName);
    CodedValueDomain codedValueDomain = (CodedValueDomain)domain;
    SortedList<object, string> codedValuePairs = await QueuedTask.Run(() =>
    {
        return codedValueDomain.GetCodedValuePairs();
    });
    return codedValuePairs;
}
   
public static async Task<Domain> GetDomainByFeatureLayerAsync(string featLayerName, string fieldName)
{           
    Domain domain = await QueuedTask.Run(() =>
    {
        var featLayer = LayerUtils.GetFeatureLayerByName(featLayerName);
        var featClass = featLayer.GetFeatureClass();
        var featClassDef = featClass.GetDefinition();
        int index = featClassDef.FindField(fieldName);
        var field = featClassDef.GetFields().ElementAt<Field>(index);
        return field.GetDomain();
    });
    return domain;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I now use a workaround: I get the domain coded value pairs when the project starts and save them in global variables. I populate my combo boxes using these global variables later in the constructor of the ProWindow. This works.

Thanks, Barbara

by Anonymous User
Not applicable

Hi Barbara Schneider

A few comment on your code.

That items could be better if you declare at viewmodel, or class level (not method level), since you are planning to reuse in both comboboxes. Might be you have solved by this way.

var items = new ObservableCollection<string>();

You might want to remove static keyword from your method. (Reason will be big discussion - would like to avoid that , I add below microsoft doco)

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-classes-...

0 Kudos
BarbaraSchneider2
Frequent Contributor

I found out what the problem is. In the constructor of my view model, I call the two functions to fill my combo boxes:

await FillStreetComboboxAsync();
await FillRiverComboboxAsync();

If I remove the "await", my two combo boxes get filled.

FillStreetComboboxAsync();
FillRiverComboboxAsync();

Here is the method FillStreetComboboxAsync():

private async void FillStreetComboboxAsync()
{          
    var items = new ObservableCollection<string>();
    _streetItems = new ReadOnlyObservableCollection<string>(items);

    object lockObject = new object(); 
    BindingOperations.EnableCollectionSynchronization(_streetItems , lockObject);
    var codeValuePairs = await GetCodeValuePairsFromDomainAsync(_layerName, _fieldName);     

    foreach (var keyValuePair in codeValuePairs)
    {
        items.Add(keyValuePair.Value);
    }                                                               
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Method fill RiverComboboxAsync() is analogous.

0 Kudos