After 2 years of learning and automating stuff using Python I am now revisiting Pro .net addins because I wasn't happy with the rather clunky UI options when I needed user input.
So what I have is two listboxes where the selected item in the first listbox defines what shows up in the second listbox. This would be easily accomplished using bindings, the rub is that to flll the second listbox I need to use and async function, which I can't call in the first listbox's SelectedItem setter. As a workaround, in the setter I call another syncronous function that runs the async function.
This works, but somehow my gut is telling me this is cheating and not the proper way to do this, and I'm not sure if this could potentially stall the UI thread if my database gets bigger.
Could somebody tell me if there's a better way to do this?
Below is the ViewModel code, the DAML would be pretty obvious.
using ArcGIS.Core.Data;
using ArcGIS.Core.Geometry;
//using ArcGIS.Core.Internal.CIM;
using ArcGIS.Desktop.Catalog;
using ArcGIS.Desktop.Core;
using ArcGIS.Desktop.Editing;
using ArcGIS.Desktop.Extensions;
using ArcGIS.Desktop.Framework;
using ArcGIS.Desktop.Framework.Contracts;
using ArcGIS.Desktop.Framework.Dialogs;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.KnowledgeGraph;
using ArcGIS.Desktop.Layouts;
using ArcGIS.Desktop.Mapping;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace BVES
{
internal class PSPSpaneViewModel : DockPane
{
private const string _dockPaneID = "BVES_PSPSpane";
private ObservableCollection<string> _lstTypes;
private ObservableCollection<string> _lstDevices;
Dictionary<String, ObservableCollection<String>> dctDevices = new();
public ICommand BtnUpdate_Click_Command { get; set; }
protected PSPSpaneViewModel()
{
InitializeDockPane();
}
private async void InitializeDockPane()
{
ObservableCollection<string> myTypes = new();
await QueuedTask.Run(() => GetTypes(myTypes));
LstTypes = myTypes;
//await QueuedTask.Run(() => GetDevices(dctDevices));
SelType = "CircuitBreaker";
}
/// <summary>
/// Show the DockPane.
/// </summary>
internal static void Show()
{
DockPane pane = FrameworkApplication.DockPaneManager.Find(_dockPaneID);
if (pane == null)
return;
pane.Activate();
}
//private int deviceCount;
public ICommand CmdSelectDevice
{
get
{
return new RelayCommand(() =>
{
if (_selDevice != null)
{
CountCustomers(_selDevice);
}
});
}
}
public async void CountCustomers(String strDevice)
{
ObservableCollection<String> lstCustomers = new();
await QueuedTask.Run(() => GetCustomers(lstCustomers, _selDevice));
MessageBox.Show(lstCustomers.Count + " Customers for device " + SelDevice);
}
private string _GDBpath = "F:/BVES/";
public string GDBpath
{
get => _GDBpath;
set => SetProperty(ref _GDBpath, value);
}
/// <summary>
/// Text shown near the top of the DockPane.
/// </summary>
private string _heading = "PSPS";
public string Heading
{
get => _heading;
set => SetProperty(ref _heading, value);
}
private string _selType = "";
public string SelType
{
get => _selType;
//set => SetProperty(ref _selType, value);
set
{
_selType = value;
NotifyPropertyChanged("SelType");
updateDevicelist();
//if (dctDevices.ContainsKey(SelType))
//{
// LstDevices = dctDevices[SelType];
//}
}
}
private string _selDevice = "";
public string SelDevice
{
get => _selDevice;
//set => SetProperty(ref _selType, value);
set
{
_selDevice = value;
NotifyPropertyChanged("SelType");
ObservableCollection<String> lstCustomers = new();
//await QueuedTask.Run(() => GetCustomer(lstCustomers,_selDevice));
}
}
/// <summary>
/// List of device types bound to the Type listbox
/// </summary>
public ObservableCollection<String> LstTypes
{
get { return _lstTypes; }
set
{
_lstTypes = value;
NotifyPropertyChanged("LstTypes");
}
}
/// <summary>
/// List devices bound to the device listbox
/// </summary>
public ObservableCollection<string> LstDevices
{
get { return _lstDevices; }
set
{
//_lstDevices = value;
//NotifyPropertyChanged("LstDevices");
SetProperty(ref _lstDevices, value, () => LstDevices);
}
}
/* public bool GetTypesHard(ObservableCollection<String> inColl)
{
inColl.Add("Fuse");
inColl.Add("TripSaver");
inColl.Add("AutoRecloser");
inColl.Add("CircuitBreaker");
return true;
}
*/
/// <summary>
/// Get a list device types. To be run as a QueuedTask.
/// </summary>
public void GetTypes(ObservableCollection<String> MyTypes)
{
Geodatabase gdb_GisData = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_GIS_Data.gdb", UriKind.Absolute)));
Geodatabase gdb_Assets = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_Assets.gdb", UriKind.Absolute)));
Table tblPSPS = gdb_GisData.OpenDataset<Table>("tblPSPS");
QueryFilter qFilt = new QueryFilter();
qFilt.SubFields = "UpType";
qFilt.PrefixClause = "DISTINCT";
qFilt.PostfixClause = "";
string strType;
using (RowCursor rowCursor = tblPSPS.Search(qFilt))
{
while (rowCursor.MoveNext())
{
using (Row current = rowCursor.Current)
{
strType = (string)current["UpType"];
MyTypes.Add(strType);
}
}
}
}
/// <summary>
/// Get a list devices of the given type. To be run as a QueuedTask.
/// </summary>
public void GetDevices(ObservableCollection<String> lstDevices, String SelType)
{
//Dictionary<String, ObservableCollection<String>> _dctDevices = new();
Geodatabase gdb_GisData = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_GIS_Data.gdb", UriKind.Absolute)));
Geodatabase gdb_Assets = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_Assets.gdb", UriKind.Absolute)));
Table tblPSPS = gdb_GisData.OpenDataset<Table>("tblPSPS");
QueryFilter qFilt = new QueryFilter();
qFilt.SubFields = "UpDevice";
qFilt.PrefixClause = "DISTINCT";
qFilt.PostfixClause = "";
string strDevice;
qFilt.WhereClause = "UpType = '" + SelType + "'";
//ObservableCollection<string> tmpColl = new ObservableCollection<string>();
lstDevices.Clear();
using (RowCursor rowCursor = tblPSPS.Search(qFilt))
{
while (rowCursor.MoveNext())
{
using (Row current = rowCursor.Current)
{
strDevice = (string)current["UpDevice"];
lstDevices.Add(strDevice);
}
}
}
}
/// <summary>
/// Run a QueuedTask to get device names and populate the device list
/// </summary>
public async void updateDevicelist()
{
ObservableCollection<String> lstDevs = new();
await QueuedTask.Run(() => GetDevices(lstDevs, _selType));
LstDevices = lstDevs;
}
/// <summary>
/// Count downstream customers given a device name.
/// </summary>
public void GetCustomers(ObservableCollection<String> MyDevices, String strDevice)
{
//Dictionary<String, ObservableCollection<String>> _dctDevices = new();
Geodatabase gdb_GisData = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_GIS_Data.gdb", UriKind.Absolute)));
Geodatabase gdb_Assets = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_Assets.gdb", UriKind.Absolute)));
Table tblPSPS = gdb_GisData.OpenDataset<Table>("tblPSPS");
QueryFilter qFilt = new QueryFilter();
qFilt.SubFields = "PREM_ID";
qFilt.PrefixClause = "";
qFilt.PostfixClause = "";
qFilt.WhereClause = "UpDevice = '" + strDevice + "'";
String strCustomer;
using (RowCursor rowCursor = tblPSPS.Search(qFilt))
{
while (rowCursor.MoveNext())
{
using (Row current = rowCursor.Current)
{
strCustomer = (string)current["UpDevice"];
MyDevices.Add(strCustomer);
}
}
}
}
/* public void GetDevicesAll(Dictionary<String, ObservableCollection<String>> _dctDevices)
{
//Dictionary<String, ObservableCollection<String>> _dctDevices = new();
Geodatabase gdb_GisData = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_GIS_Data.gdb", UriKind.Absolute)));
Geodatabase gdb_Assets = new Geodatabase(new FileGeodatabaseConnectionPath(new Uri(_GDBpath + "BVES_Assets.gdb", UriKind.Absolute)));
Table tblPSPS = gdb_GisData.OpenDataset<Table>("tblPSPS");
QueryFilter qFilt = new QueryFilter();
qFilt.SubFields = "UpDevice";
qFilt.PrefixClause = "DISTINCT";
qFilt.PostfixClause = "";
string strDevice;
foreach (string itmType in LstTypes)
{
qFilt.WhereClause = "UpType = '" + itmType + "'";
ObservableCollection<string> tmpColl = new ObservableCollection<string>();
using (RowCursor rowCursor = tblPSPS.Search(qFilt))
{
while (rowCursor.MoveNext())
{
using (Row current = rowCursor.Current)
{
strDevice = (string)current["UpDevice"];
tmpColl.Add(strDevice);
}
}
_dctDevices.Add(itmType, tmpColl);
}
}
}
*/
/// <summary>
/// Button implementation to show the DockPane.
/// </summary>
}
internal class PSPSpane_ShowButton : Button
{
protected override void OnClick()
{
PSPSpaneViewModel.Show();
}
}
}
Hi,
The simplest way is to call your async method from property setter like this:
private string _selectedContinent;
public string SelectedContinent
{
get => _selectedContinent;
set
{
if (SetProperty(ref _selectedContinent, value))
{
_ = LoadCountriesAsync(value);
}
}
}Continents will be first list, countries will be second list. In the same way you can call async methods from class constructor.
You can add more functionality as cancelation token, loading status and etc.
internal class WorldViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Continents { get; } = new ObservableCollection<string>();
public ObservableCollection<string> Countries { get; } = new ObservableCollection<string>();
private string _selectedContinent;
public string SelectedContinent
{
get => _selectedContinent;
set
{
if (SetProperty(ref _selectedContinent, value))
{
// Cancel any in-flight load and start a new one (fire-and-forget)
_loadCts?.Cancel();
_loadCts = new CancellationTokenSource();
var token = _loadCts.Token;
_ = LoadCountriesAsync(value, token);
}
}
}
private string _selectedCountry;
public string SelectedCountry
{
get => _selectedCountry;
set => SetProperty(ref _selectedCountry, value);
}
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
private CancellationTokenSource _loadCts;
public WorldViewModel()
{
InitializeContinents();
}
private void InitializeContinents()
{
// Seed continents - replace with real data source if needed
Continents.Add("Africa");
Continents.Add("Asia");
Continents.Add("Europe");
Continents.Add("North America");
Continents.Add("South America");
Continents.Add("Oceania");
Continents.Add("Antarctica");
}
public async Task LoadCountriesAsync(string continent, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(continent))
{
Application.Current.Dispatcher.Invoke(() => Countries.Clear());
return;
}
try
{
IsLoading = true;
// Simulate async IO. Replace with your real async API call (pass cancellationToken).
await Task.Delay(500, cancellationToken);
// Example data - replace with real result
var result = new[]
{
$"{continent} - Country A",
$"{continent} - Country B",
$"{continent} - Country C"
};
// Update UI collection on UI thread
Application.Current.Dispatcher.Invoke(() =>
{
Countries.Clear();
foreach (var c in result) Countries.Add(c);
});
}
catch (OperationCanceledException)
{
// expected on cancellation - ignore
}
catch (Exception ex)
{
// Adjust logging/error handling to your project's policy
System.Diagnostics.Debug.WriteLine($"LoadCountriesAsync error: {ex}");
}
finally
{
IsLoading = false;
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value)) return false;
storage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
#endregion
}Don't forget to update lists on the UI thread to avoid threading issues.
P.s. Code was generated by GitHub Copilot