How to programatically get a TabControl's Children - c#

I have the following code that used to work for me. I'm using this as a placeholder as I always end up nesting a TabControls within another one.
public partial class MyTabControl : TabControl {
public TabControl _tc { get { return ((Grid)Content).Children[0] as TabControl; } }
}
I recently updated a few nuget packages however (MahApps.Metro and its dependencies) and now I'm getting the error The name 'Content' does not exist in the current context.
I'm not sure if the nuget package should have affected this though as I'm subclassing WPF's TabControl. Does anyone know of a workaround to get the same result? I basically just want to be able to programatically call on the TabControl's children.

A TabControl has a SelectedContent property that returns the content of the currently selected tab:
public partial class MyTabControl : TabControl
{
public TabControl _tc { get { return ((Grid)SelectedContent).Children[0] as TabControl; } }
}
For this to work, it assumes that the selected tab actually contains a Grid with a nested TabControl:
<local:MyTabControl>
<TabItem>
<Grid>
<TabControl />
</Grid>
</TabItem>
</local:MyTabControl>

Related

ObservableCollections not working yet in MAUI?

I tried MAUI and MVVM with an ObservableCollection.
Created new MAUI project
Added ViewModel with BindingContext
Bound StackLayout to an ObservableCollection
Added Item to the Collection on Button click
Nothing changes. "Foo" and "Bar" still here although a third element is added on button click.
My project:
Replaced first label in the (default) new project with
<StackLayout
Grid.Row="0"
BindableLayout.ItemsSource="{Binding Foo}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label Text="{Binding .}"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
Added
<ContentPage.BindingContext>
<local:MainPageViewModel/>
</ContentPage.BindingContext>
ViewModel:
public class MainPageViewModel
{
public ObservableCollection<string> Foo { get; } = new() { "Foo", "Bar" };
}
Added in the button click (added as first element to avoid missing changes because of UI height problems, but nothing changes when I try Add() instead):
(this.BindingContext as MainPageViewModel).Foo.Insert(0, "Baz");
Is there any info if ObservableCollections should work already?
Or (coming from WPF) is there something I have to change for MAUI/Xamarin?
I've tried to reproduce what you're seeing and I 100% can. The initial value shows up, but not when adding something. Inspecting the Live Visual Tree though, we see that the Labels do actually get added. The ObservableCollection works, it seems the layout just doesn't update.
Even with a call to force the layout on the StackLayout it won't update to it seems, so it looks like the layout isn't working properly at this point.
Looking at the roadmap there is still some layout work to do, it will probably be fixed for preview 7.
Here is a solution that works.
I suggest to create a model.
Your model should implement ObservableObject.
Public class Post: ObservableObject
{
private string title= String.Empty;
public string Title
{
get
{
return this.title;
}
set
{
SetProperty(ref title, value);
}
}
}
Your view model should implement ObservableObject
public partial class PostViewModel : ObservableObject
{
[ObservableProperty]
ObservableCollection<Post> posts;
}
Afterwards, any modification on the property Title will render the ui accordingly.
(Tested on Visual Studio 17.3.6)
More information can be found here.

Is there a way of using virtualization with hidden panels or expanders?

I'm trying to improve performance with my WPF application and I'm having problems with a complex ItemsControl. Although I've added Virtualization, there's still a performance problem and I think I've worked out why.
Each item contains a series of expandable areas. So the user sees a summary at the start but can drill down by expanding to see more information. Here's how it looks:
As you can see, there's some nested ItemsControls. So each of the top level items has a bunch of Hidden controls. The virtualization prevents off-screen items from loading, but not the hidden items within the items themselves. As a result, the relatively simple initial layout takes a significant time. Flicking around some of these views, 87% of time is spent parsing and Layout, and it takes a few seconds to load.
I'd much rather have it take 200ms to expand when (if!) the user decides to, rather than 2s to load the page as a whole.
Asking for advice really. I can't think of a nice way of adding the controls using MVVM however. Is there any expander, or visibility based virtualization supported in WPF or would I be creating my own implementation?
The 87% figure comes from the diagnostics:
If you simply have
- Expander
Container
some bindings
- Expander
Container
some bindings
+ Expander
+ Expander
... invisible items
Then yes, Container and all bindings are initialized at the moment when view is displayed (and ItemsControl creates ContentPresenter for visible items).
If you want to virtualize content of Expander when it's collapsed, then you can use data-templating
public ObservableCollection<Item> Items = ... // bind ItemsControl.ItemsSource to this
class Item : INotifyPropertyChanged
{
bool _isExpanded;
public bool IsExpanded // bind Expander.IsExpanded to this
{
get { return _isExpanded; }
set
{
Data = value ? new SubItem(this) : null;
OnPropertyChanged(nameof(Data));
}
}
public object Data {get; private set;} // bind item Content to this
}
public SubItem: INotifyPropertyChanged { ... }
I hope there is no need to explain how to to do data-templating of SubItem in xaml.
If you do that then initially Data == null and nothing except Expander is loaded. As soon as it's expanded (by user or programmatically) view will create visuals.
I thought I'd put the details of the solution, which is pretty much a direct implementation of Sinatr's answer.
I used a content control, with a very simple data template selector. The template selector simply checks if the content item is null, and chooses between two data templates:
public class VirtualizationNullTemplateSelector : DataTemplateSelector
{
public DataTemplate NullTemplate { get; set; }
public DataTemplate Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == null)
{
return NullTemplate;
}
else
{
return Template;
}
}
}
The reason for this is that the ContentControl I used still lays out the data template even if the content is null. So I set these two templates in the xaml:
<ContentControl Content="{Binding VirtualizedViewModel}" Grid.Row="1" Grid.ColumnSpan="2" ><!--Visibility="{Binding Expanded}"-->
<ContentControl.Resources>
<DataTemplate x:Key="Template">
<StackPanel>
...complex layout that isn't often seen...
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="NullTemplate"/>
</ContentControl.Resources>
<ContentControl.ContentTemplateSelector>
<Helpers:VirtualizationNullTemplateSelector Template="{StaticResource Template}" NullTemplate="{StaticResource NullTemplate}"/>
</ContentControl.ContentTemplateSelector>
</ContentControl>
Finally, rather than using a whole new class for a sub-item, it's pretty simple to create a "VirtualizedViewModel" object in your view model that references "this":
private bool expanded;
public bool Expanded
{
get { return expanded; }
set
{
if (expanded != value)
{
expanded = value;
NotifyOfPropertyChange(() => VirtualizedViewModel);
NotifyOfPropertyChange(() => Expanded);
}
}
}
public MyViewModel VirtualizedViewModel
{
get
{
if (Expanded)
{
return this;
}
else
{
return null;
}
}
}
I've reduced the 2-3s loading time by about by about 75% and it seems much more reasonable now.
This simple solution helped me:
<Expander x:Name="exp1">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp1, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
<Expander x:Name="exp2">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp2, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
An easier way to achieve this is to change the default Visibility of the contents to Collapsed. In this case WPF won't create it initially, but only when a Trigger sets it to Visible:
<Trigger Property="IsExpanded" Value="true">
<Setter Property="Visibility"Value="Visible" TargetName="ExpandSite"/>
</Trigger>
Here "ExpandSite" is the ContentPresenter within the default ControlTemplate of the Expander control.
Note that this has been fixed in .NET - see the default style from the WPF sources on github.
In case you have an older version, you can still use this fixed control template to update the old one with an implicit style.
You can apply the same technique to any other panel or control.
It's easy to check if the control was already created with Snoop. Once you attached it to your application, you can filter the visual tree with the textbox on the top left. If you don't find one control in the tree, it means it was not created yet.

How do I have the Click event of a button manipulate another control in MVVM

I am using WPF(4.5) and Caliburn.Micro. I am trying to understand how to have an "event" in my View manipulate other controls in my View.
For instance:
My View has an Expander Control, a Button, and a GridView. The GridView is inside the Expander. When the user clicks the button it calls a method in the VM that populates the gridview with a BindableCollection<>. What I want to have happen is when that collection has more then 1 item I want to Expand the Expander Control automatically.
Ideas?
You can bind to the number of items in a collection:
<Expander IsExpanded="{Binding Path=YourCollection.Length, Converter={StaticResource ResourceName=MyConverter}" />
and then in the window or usercontrol:
<UserControl... xmlns:converters="clr-namespace:My.Namespace.With.Converters">
<UserControl.Resources>
<converters:ItemCountToBooleanConverter x:Key="MyConverter" />
</UserControl.Resources>
</UserControl>
and the converter:
namespace My.Namespace.With.Converters {
public class ItemCountToBooleanConverter : IValueConverter
{
// implementation of IValueConverter here
...
}
}
I wrote this out of my head, so apologies if it contains errors ;)
Also: Make sure your viewModel implements the INotifyPropertyChanged interface, but I assume you already know that.
#cguedel method is completely valid but if you don't want to use Converters (why one more class) then in your view model have another property of type bool maybe called ShouldExpand, well why talk so much, let me show you:
class YourViewModel {
public bool ShouldExpand {
get {
return _theCollectionYouPopulatedTheGridWith.Length() != 0;
// or maybe use a flag, you get the idea !
}
}
public void ButtonPressed() {
// populate the grid with collection
// NOW RAISE PROPERTY CHANGED EVENT FOR THE ShouldExpand property
}
}
Now in your View use this binding instead:
<Expander IsExpanded="{Binding Path=ShouldExpand}" />
As i said before the other solution is well but i like to reduce the number of classes in my solutions. This is just another way of doing it.

WPF Caliburn.Micro/mvvm Navigation

I'm building a project, and one of the biggest problems I've come across until now is navigation.
I've been looking for some time now for examples of caliburn.micro/mvvm navigation, but they all seem to be really long and I couldn't really understand much of it (beginner here!).
Some info about my project:
I want there to be an outer window/shell, with menu links/tabs that open pages according to the button clicked inside an inner part of the shell, and be able to open change the page from within a one.
I currently have: ShellViewModel.cs, MainViewModel.cs, my models, and my views.
For now, all I need to know is how to make MainViewModel load inside shellviewmodel on startup(using contentcontrol/frames...), and how to move from one page to another.
You could also just write it in points, and link me to some useful examples, and I believe I could continue from there. It'd be best to get a thorough explanation of stuff if possible.
Have a read about Conductors and Screens on the official documentation.
As a simple example, your ShellViewModel could be a Conductor of one active screen (i.e. only one screen becomes active/inactive at a time):
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
You can then set the ActiveItem of the Conductor to the view model instance that you wish to be currently active:
this.ActivateItem(myMainViewModel);
A collection Conductor type also provides an Items collection which you can populate as you instantiate new windows. Viewmodels in this Items collection may be those that are currently deactivated but not yet closed, and you can activate them by using ActivateItem as above. It also makes it very easy to create a menu of open windows by using an ItemsControl with x:Name="Items" in your ShellView.
Then, to create the ShellView, you can use a ContentControl and set its name to be the same as the ActiveItem property, and Caliburn.Micro will do the rest:
<ContentControl x:Name="ActiveItem" />
You can then respond to activation/deactivation in your MainViewModel by overriding OnActivate/OnDeactivate in that class.
In ShellView you use a content control like this:
<ShellView xmlns:cal="http://caliburnproject.org/">
<StackPanel>
<Button Content="Show other view" cal:Message.Attach="ShowOtherView" />
<ContentControl cal:View.Model="{Binding Child}" />
</StackPanel>
</ShellView>
ShellViewModel:
public class ShellViewModel : Screen
{
private object Child;
public object Child
{
get{ return child; }
set
{
if(child == value)
return;
child = value;
NotifyOfPropertyChange(() => Child);
}
}
public ShellViewModel()
{
this.Child = new MainViewModel();
}
public void ShowOtherView()
{
this.Child = new FooViewModel();
}
}
So this is a very basic example. But as you see, your ShellView provides a ContentControl, which shows the child view. This ContentControl is bound via View.Model to the Child property from your ShellViewModel.
In ShellView, I used a button to show a different view, but you can also use a menu or something like that.

Why doesn't this command binding work?

I have a WPF application which has a main window composed from several custom UserControls placed in AvalonDock containers.
I want some of the UserControls' functionality to be accessible from a toolbar and menubar in the main window. I have a command defined as this in the control like this:
public ICommand UnfoldAllCommand
{
get
{
if (this.unfoldAllCommand == null)
{
this.unfoldAllCommand = new RelayCommand(param => this.UnfoldAll());
}
return unfoldAllCommand;
}
}
Now I have this UserControl defined in main window XAML under name "editor"
<local:Editor x:Name="editor" />
This control is also made public via Edtor property of the main window (the window is its own DataContext).
public Editor Editor { get { return this.editor; } }
The menubar is located in the main window XAML. This definition definition of one MenuItem which triggers the UserControl's UnfoldAll command works perfectly.
<MenuItem Header="Unfold All" Command="{Binding UnfoldAllCommand, ElementName=editor}" InputGestureText="Ctrl+U" />
However, this definition is arguably prettier, but it doesn't work (the MenuItem is clickable, but won't fire the UnfoldAll method):
<MenuItem Header="Unfold All" Command="{Binding Editor.UnfoldAllCommand}" InputGestureText="Ctrl+U" />
Why?
Your binding looks at the DataContext, and your last binding says: Whatevers is on the DataContext, get me the property Editor, and then the property UnfoldAllCommand.
Your first binding is therefore correct.
You could set the Editor on the DataContext in code behind, en change the Binding to just UnfoldAllCommand.
After the InitializeComponents() place:
DataContext = this;
The problem was that for {Binding Editor.Property} to work, Editor must be a Dependency Property as well (not only Property).

Categories