Custom Navigation Control
-
Using WPF .Core 6 I am trying to create this custom control[^]. I'm open to a better way if anyone has one. There is the outer Navigation Container, with Navigation Panes inside it. There can be any number of Navigation Panes. Each Navigation Pane will contain Navigation Items as links that the user can click on. What I would like to happen when the Window shows is for the container to be there, and all the panes added and showing a spinning indicator that is visible as long as the pane is loading. Each pane must load independant of the other, and each could take any amount of time to load. So far, the Navigation Container displays, and each NavigationPane shows, and the code begind for the Navigation Pane is receiving its data, but nothing shows up. And, the Spinning Indicator never goes away. The problem seems to be in the NavigationPane. The NavigationContainer is working as expected Here's what I'm getting right now.[^] I'm NOT getting any binding errors, and all relevant code is being hit - I just don't see anything. Here's the NavigationContainer's Load() method. It adds each pane first, then retrieves the data for each. This all runs, and the data is assigned to the Items DP on the NavigationPane
private async Task Load()
{
if (NavigationPanes != null)
{
// Add a pane to the collection for each NavigationItemModel. This shows the panes
// to the user BEFORE starting the data download
foreach (var navigationPaneModel in NavigationPanes)
{
var pane = new NavigationPane
{
Header = navigationPaneModel.Header ?? "",
NavigationItemType = navigationPaneModel.NavigationItemType
};
ContainerItems.Add(pane);
}if (\_apiProxy != null) { // Go through each pane model foreach (var navigationPaneModel in NavigationPanes) { await Task.Run(() => { // Go th
-
Using WPF .Core 6 I am trying to create this custom control[^]. I'm open to a better way if anyone has one. There is the outer Navigation Container, with Navigation Panes inside it. There can be any number of Navigation Panes. Each Navigation Pane will contain Navigation Items as links that the user can click on. What I would like to happen when the Window shows is for the container to be there, and all the panes added and showing a spinning indicator that is visible as long as the pane is loading. Each pane must load independant of the other, and each could take any amount of time to load. So far, the Navigation Container displays, and each NavigationPane shows, and the code begind for the Navigation Pane is receiving its data, but nothing shows up. And, the Spinning Indicator never goes away. The problem seems to be in the NavigationPane. The NavigationContainer is working as expected Here's what I'm getting right now.[^] I'm NOT getting any binding errors, and all relevant code is being hit - I just don't see anything. Here's the NavigationContainer's Load() method. It adds each pane first, then retrieves the data for each. This all runs, and the data is assigned to the Items DP on the NavigationPane
private async Task Load()
{
if (NavigationPanes != null)
{
// Add a pane to the collection for each NavigationItemModel. This shows the panes
// to the user BEFORE starting the data download
foreach (var navigationPaneModel in NavigationPanes)
{
var pane = new NavigationPane
{
Header = navigationPaneModel.Header ?? "",
NavigationItemType = navigationPaneModel.NavigationItemType
};
ContainerItems.Add(pane);
}if (\_apiProxy != null) { // Go through each pane model foreach (var navigationPaneModel in NavigationPanes) { await Task.Run(() => { // Go th
My immediate thought is that the problem is in the task continuation. Have you checked the result of the FirstOrDefault, to make sure that it is actually finding the container?
-
Using WPF .Core 6 I am trying to create this custom control[^]. I'm open to a better way if anyone has one. There is the outer Navigation Container, with Navigation Panes inside it. There can be any number of Navigation Panes. Each Navigation Pane will contain Navigation Items as links that the user can click on. What I would like to happen when the Window shows is for the container to be there, and all the panes added and showing a spinning indicator that is visible as long as the pane is loading. Each pane must load independant of the other, and each could take any amount of time to load. So far, the Navigation Container displays, and each NavigationPane shows, and the code begind for the Navigation Pane is receiving its data, but nothing shows up. And, the Spinning Indicator never goes away. The problem seems to be in the NavigationPane. The NavigationContainer is working as expected Here's what I'm getting right now.[^] I'm NOT getting any binding errors, and all relevant code is being hit - I just don't see anything. Here's the NavigationContainer's Load() method. It adds each pane first, then retrieves the data for each. This all runs, and the data is assigned to the Items DP on the NavigationPane
private async Task Load()
{
if (NavigationPanes != null)
{
// Add a pane to the collection for each NavigationItemModel. This shows the panes
// to the user BEFORE starting the data download
foreach (var navigationPaneModel in NavigationPanes)
{
var pane = new NavigationPane
{
Header = navigationPaneModel.Header ?? "",
NavigationItemType = navigationPaneModel.NavigationItemType
};
ContainerItems.Add(pane);
}if (\_apiProxy != null) { // Go through each pane model foreach (var navigationPaneModel in NavigationPanes) { await Task.Run(() => { // Go th
Kevin Marois wrote:
Each pane must load independant of the other
That doesn't seem to tally with:
Kevin Marois wrote:
foreach (var navigationPaneModel in NavigationPanes)
{
await Task.Run(() =>
{
// Go the the back end & get the data
return _apiProxy.GetNavigationItems(navigationPaneModel.NavigationItemType);
}).ContinueWith(task =>
{
App.Current.Dispatcher.BeginInvoke(() =>
{
// Find the container for the data
var container = ContainerItems.FirstOrDefault(x => x.NavigationItemType == navigationPaneModel.NavigationItemType);
if (container != null && task.Result != null)
{
// Assign the data to it
container.Items = new ObservableCollection(task.Result);
}
});
});
}Your loop kicks off a task to load each pane, then waits for that task to complete before trying to load the next pane. Try extracting the "load a pane" code to a separate method:
private async Task LoadPane(NavigationPane navigationPaneModel)
{
var result = await Task.Run(() => _apiProxy.GetNavigationItems(navigationPaneModel.NavigationItemType);
if (result is null) return;var container = ContainerItems.FirstOrDefault(x => x.NavigationItemType == navigationPaneModel.NavigationItemType); if (container is null) return; container.Items = new ObservableCollection<NavigationEntity>(result);
}
Then change the loop to:
List<Task> tasks = new List<Task>(NavigationPanes.Count);
foreach (var navigationPaneModel in NavigationPanes)
{
tasks.Add(LoadPane(navigationPaneModel));
}await Task.WhenAll(tasks);
Kevin Marois wrote:
control.IsBusyVisible = Visibility.Collapsed; //<==== THIS IS BEING HIT, BUT THE INDICATOR STILL SHOWS. AND NO DATA ITEMS APPEAR
You've declared the
IsBusyVisible
property as a regular property, but yourNavigationPane
class is aDependencyObject
. The WPF binding system will only observe changes toDependencyProperty
properties on aDependencyObject
-derived class. Change the property to a dependency property:publ
-
Kevin Marois wrote:
Each pane must load independant of the other
That doesn't seem to tally with:
Kevin Marois wrote:
foreach (var navigationPaneModel in NavigationPanes)
{
await Task.Run(() =>
{
// Go the the back end & get the data
return _apiProxy.GetNavigationItems(navigationPaneModel.NavigationItemType);
}).ContinueWith(task =>
{
App.Current.Dispatcher.BeginInvoke(() =>
{
// Find the container for the data
var container = ContainerItems.FirstOrDefault(x => x.NavigationItemType == navigationPaneModel.NavigationItemType);
if (container != null && task.Result != null)
{
// Assign the data to it
container.Items = new ObservableCollection(task.Result);
}
});
});
}Your loop kicks off a task to load each pane, then waits for that task to complete before trying to load the next pane. Try extracting the "load a pane" code to a separate method:
private async Task LoadPane(NavigationPane navigationPaneModel)
{
var result = await Task.Run(() => _apiProxy.GetNavigationItems(navigationPaneModel.NavigationItemType);
if (result is null) return;var container = ContainerItems.FirstOrDefault(x => x.NavigationItemType == navigationPaneModel.NavigationItemType); if (container is null) return; container.Items = new ObservableCollection<NavigationEntity>(result);
}
Then change the loop to:
List<Task> tasks = new List<Task>(NavigationPanes.Count);
foreach (var navigationPaneModel in NavigationPanes)
{
tasks.Add(LoadPane(navigationPaneModel));
}await Task.WhenAll(tasks);
Kevin Marois wrote:
control.IsBusyVisible = Visibility.Collapsed; //<==== THIS IS BEING HIT, BUT THE INDICATOR STILL SHOWS. AND NO DATA ITEMS APPEAR
You've declared the
IsBusyVisible
property as a regular property, but yourNavigationPane
class is aDependencyObject
. The WPF binding system will only observe changes toDependencyProperty
properties on aDependencyObject
-derived class. Change the property to a dependency property:publ
Thanks. This make much more sense. So I extracted all the Nav stuff into a demo project to get it out of my app, and I still can get this to work. It looks like the panes never get added. I put it in a repo here[^]. I would appreciate more help if you wouldn't mind.
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
-
Thanks. This make much more sense. So I extracted all the Nav stuff into a demo project to get it out of my app, and I still can get this to work. It looks like the panes never get added. I put it in a repo here[^]. I would appreciate more help if you wouldn't mind.
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
I was going to take a look, but it looks like your repo is private.
-
I was going to take a look, but it looks like your repo is private.
Oops. It's public now. Thanks Pete!
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
-
Oops. It's public now. Thanks Pete!
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
I'll have a look in the morning.
-
I'll have a look in the morning.
Hey Pete, just following up to see if had a chance to look at this?
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
-
Hey Pete, just following up to see if had a chance to look at this?
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
I have spent some time digging into your sample and I am at a loss. I can't see anything obvious in there, and it's an absolute stumper.
-
Thanks. This make much more sense. So I extracted all the Nav stuff into a demo project to get it out of my app, and I still can get this to work. It looks like the panes never get added. I put it in a repo here[^]. I would appreciate more help if you wouldn't mind.
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
The first obvious issue: the
ContainerItems
property is not a dependency property. As I said, for aDependencyObject
-derived class, WPF will only observe property changes for dependency properties.public static readonly DependencyProperty ContainerItemsProperty
= DependencyProperty.Register("ContainerItems",
typeof(List),
typeof(NavigationContainer),
new PropertyMetadata(null));public List ContainerItems
{
get { return (List)GetValue(ContainerItemsProperty); }
set { SetValue(ContainerItemsProperty, value); }
}Next problem: since you're using a
List<T>
rather than anObservableCollection<T>
, WPF will only notice when you set the property. Since you do that before adding any items to the list, WPF will never notice the panes being loaded. Change the code to set the property after populating the list:private async Task Load()
{
if (NavigationPanes != null)
{
var items = new List();List tasks = new List(NavigationPanes.Count); foreach (var navigationPaneModel in NavigationPanes) { tasks.Add(LoadPane(navigationPaneModel, items)); } await Task.WhenAll(tasks); ContainerItems = items; }
}
private async Task LoadPane(NavigationPaneModel navigationPaneModel, List containerItems)
With those changes in place (and dropping the
Thread.Sleep
values so it loads in a reasonable time), I now get four expanders in the list. However, although they have different headers, none of them contain any data. Looking in theGeneric.xaml
file, you're binding theHeader
property, but not theItems
property:Once you add
Items="{Binding Items}"
to that, the listboxes are now populated.
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
-
The first obvious issue: the
ContainerItems
property is not a dependency property. As I said, for aDependencyObject
-derived class, WPF will only observe property changes for dependency properties.public static readonly DependencyProperty ContainerItemsProperty
= DependencyProperty.Register("ContainerItems",
typeof(List),
typeof(NavigationContainer),
new PropertyMetadata(null));public List ContainerItems
{
get { return (List)GetValue(ContainerItemsProperty); }
set { SetValue(ContainerItemsProperty, value); }
}Next problem: since you're using a
List<T>
rather than anObservableCollection<T>
, WPF will only notice when you set the property. Since you do that before adding any items to the list, WPF will never notice the panes being loaded. Change the code to set the property after populating the list:private async Task Load()
{
if (NavigationPanes != null)
{
var items = new List();List tasks = new List(NavigationPanes.Count); foreach (var navigationPaneModel in NavigationPanes) { tasks.Add(LoadPane(navigationPaneModel, items)); } await Task.WhenAll(tasks); ContainerItems = items; }
}
private async Task LoadPane(NavigationPaneModel navigationPaneModel, List containerItems)
With those changes in place (and dropping the
Thread.Sleep
values so it loads in a reasonable time), I now get four expanders in the list. However, although they have different headers, none of them contain any data. Looking in theGeneric.xaml
file, you're binding theHeader
property, but not theItems
property:Once you add
Items="{Binding Items}"
to that, the listboxes are now populated.
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
It's working now. Thanks Richard.
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.
-
The first obvious issue: the
ContainerItems
property is not a dependency property. As I said, for aDependencyObject
-derived class, WPF will only observe property changes for dependency properties.public static readonly DependencyProperty ContainerItemsProperty
= DependencyProperty.Register("ContainerItems",
typeof(List),
typeof(NavigationContainer),
new PropertyMetadata(null));public List ContainerItems
{
get { return (List)GetValue(ContainerItemsProperty); }
set { SetValue(ContainerItemsProperty, value); }
}Next problem: since you're using a
List<T>
rather than anObservableCollection<T>
, WPF will only notice when you set the property. Since you do that before adding any items to the list, WPF will never notice the panes being loaded. Change the code to set the property after populating the list:private async Task Load()
{
if (NavigationPanes != null)
{
var items = new List();List tasks = new List(NavigationPanes.Count); foreach (var navigationPaneModel in NavigationPanes) { tasks.Add(LoadPane(navigationPaneModel, items)); } await Task.WhenAll(tasks); ContainerItems = items; }
}
private async Task LoadPane(NavigationPaneModel navigationPaneModel, List containerItems)
With those changes in place (and dropping the
Thread.Sleep
values so it loads in a reasonable time), I now get four expanders in the list. However, although they have different headers, none of them contain any data. Looking in theGeneric.xaml
file, you're binding theHeader
property, but not theItems
property:Once you add
Items="{Binding Items}"
to that, the listboxes are now populated.
"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer
Richard, after implementing your changes, the loading works fine, but I now have this problem[^] with the expander headers. I would appreciate your input.
If it's not broken, fix it until it is. Everything makes sense in someone's mind. Ya can't fix stupid.