Home > Enterprise >  WPF (Avalonia) - Draw control (button) outside TabItem?
WPF (Avalonia) - Draw control (button) outside TabItem?

Time:07-29

Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button. Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).

So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so enter image description here

(if you look hard you can see where the button is as the header covering it is slightly translucent)

Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.

My MainWindow.axaml:

<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">                   
    <TabItem Header="Current" ZIndex="1">
        <ScrollViewer Classes="CrawlersInAction">
            <StackPanel>
                <Canvas>
                    <Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
                </Canvas>
                <!-- My very long template code for rendering the expanders -->
            </StackPanel>
        </ScrollViewer>
    </TabItem>
</TabControl>

I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.

PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.

CodePudding user response:

If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.

<Grid>
   <TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>

An Instance of MyTabViewModel has a collection on it:

public ObservableCollection<MyCollectionType> Items

The item class MyCollectionType has an IsExpanded property ...

public bool IsExpanded {get;set;}

Bound to your Expander control IsExpanded property.

Shove your button into the XAML

<Grid>
   <TabControl Items="{Binding MyTabViewModels}" />
   <Button Commmand={Binding ExpandAllCommand} />
</Grid>

Now on your base ViewModel your ICommand can do something like:

public void ExpandAllCommandExecuted()
{
    foreach(var vm in SelectedTab.Items)
    {
        vm.IsExpanded = true;
    }
}

Good luck, this is all pseudocode but illustrates a potential pattern.

CodePudding user response:

The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).

My XAML is now:

<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
    <TabItem Header="Current">
        <StackPanel>
            <Canvas>
                <StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
                    <Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
                    <Button Command="{Binding ExpandAll}"   IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
                </StackPanel>
            </Canvas>

            <ScrollViewer Classes="CrawlersInAction">
                <StackPanel>
                    <ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
                                    <!-- Ommited my very long template code -->
                                </Expander>
                            <DataTemplate>
                        <ItemsControl.ItemTemplate>
                    <ItemsControl>
                </StackPanel>
            </ScrollViewer>
        </StackPanel>
    </TabItem>
</TabControl>

Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:

    public class SiteInfo : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        public static readonly bool StartIsExpanded = true;
        private bool _isExpanded = StartIsExpanded;
        public bool IsExpanded
        {
            get { return _isExpanded; }
            set
            {
                if (value != IsExpanded)
                {
                    _isExpanded = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
                }
            }
        }

When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged = SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.


    public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    {

        public new event PropertyChangedEventHandler? PropertyChanged;
        public ObservableCollection<SiteInfo> SiteInfos { get; } 
            = new ObservableCollection<SiteInfo>();

        //Change SiteInfo.StartExpanded if you want this changed.
        private bool _allAreExpanded = SiteInfo.StartIsExpanded;
        public bool AllAreExpanded
        {
            get => _allAreExpanded;
            set
            {
                if (_allAreExpanded != value)
                {
                    _allAreExpanded = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
                }
            }
        }
        //Change SiteInfo.StartExpanded if you want this changed.
        private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
        public bool AllAreCollapsed {
            get { return _allAreCollapsed; }
            set {
                if (_allAreCollapsed != value)
                {
                    _allAreCollapsed = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
                }
            }
        }

        private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            if(e.PropertyName == nameof(siteInfo.IsExpanded))
            {
                AllAreCollapsed = AreAllCollapsed();
                AllAreExpanded = AreAllExpanded();
            }
        }

        public bool AreAllCollapsed()
        {
            return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
        }

        public bool AreAllExpanded()
        {
            return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
        }

        public void CollapseAll()
        {
            foreach(SiteInfo siteInfo in SiteInfos)
            {
                siteInfo.IsExpanded = false;
            }
        }
        public void ExpandAll()
        {
            foreach (SiteInfo siteInfo in SiteInfos)
            {
                siteInfo.IsExpanded = true;
            }
        }
    }

Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.

So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button. Buttons where I want them to be and disabling/enabling as needed

  • Related