Search code examples
c#comboboxcode-behindavaloniaui

How can I specify a DataContext to be the code-behind module for any given xaml? (converting 'Avalonia ComboBox' Example)


Sorry another beginner question.
I am trying to include an example from the Avalonia Documentation into my test project but I am getting stuck on trying to use MainWindowViewModel as well as code-behind for MainWindow (MainWindow.axaml.cs)

The example is the last one on https://docs.avaloniaui.net/docs/reference/controls/combobox which is unfortunately a little out of date.

    <StackPanel Margin="20">
      <ComboBox x:Name="fontComboBox" SelectedIndex="0"
                Width="200" MaxDropDownHeight="300">
        <ComboBox.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding Name}" FontFamily="{Binding}" />
          </DataTemplate>
        </ComboBox.ItemTemplate>
      </ComboBox>
    </StackPanel>

using Avalonia.Controls;
using Avalonia.Media;
using System.Linq;

namespace AvaloniaControls.Views
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();            
            fontComboBox.Items = FontManager.Current
                .GetInstalledFontFamilyNames()
                .Select(x => new FontFamily(x))
                .OrderBy(x=>x.Name);
            fontComboBox.SelectedIndex = 0;
        }
    }
}

The MainWindow has several other controls using MVVM view model

x:DataType="vm:MainWindowViewModel"

so whenever I type {Binding } rider helpfully reminds me with '(MainWindowViewModel). Path=' I have also learnt that when you use the term 'Binding' it must be to a Property - unless, perhaps, this is in the Code Behind. (Which is just doing my head in when trying to populate, in this case, a ComboBox.)

I think I have sucessfully changed the code-behind to... (at least in debug lFonts contains the data I expect)

public List<string> lFonts = new List<string>();
public MainWindow()
 {
     InitializeComponent();
     ColourComboBox.ItemsSource = ColourBasesTxt;
     ThemesComboBox.ItemsSource = Themes;

    IFontCollection allFonts = FontManager.Current.SystemFonts;
    foreach (var font in allFonts)
    {
        lFonts.Add(font.Name);
    }
    FontComboBox.ItemsSource = lFonts;

Now what I cant workout is how to link this to the xaml. The example code had

  <ComboBox x:Name="fontComboBox" SelectedIndex="0"
            Width="200" MaxDropDownHeight="300">
    <ComboBox.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding Name}" FontFamily="{Binding}" />
      </DataTemplate>
    </ComboBox.ItemTemplate>
  </ComboBox>

in my code

  <ComboBox x:Name="FontComboBox"   Grid.Column="1"  Grid.Row="3"
            HorizontalAlignment="Left" HorizontalContentAlignment="Left" VerticalAlignment="Center" 
            MaxDropDownHeight="300" Width="150" Height="40" FontSize="16" 
            SelectedItem="{ Binding SelectedFont}" >
      <ComboBox.ItemTemplate>
          <DataTemplate>
              <TextBlock Text="{Binding Name}" />
          </DataTemplate>
      </ComboBox.ItemTemplate>
  </ComboBox>

Now

<TextBlock Text="{Binding Name}" />

does not work because its trying to bind to MainWindowViewModel.Name
I have tried many crazy suggestions to get the data context within the DataTemplate to be the code-behind.
All but 1 did not pass the syntax check. I cant refocus the whole ComboBox because the 'selectedItem' needs to be to a property in MainWindowViewModel (which it is).

The one that got close is

<TextBlock Text="{Binding ElementName=FontComboBox}" />

That actually ran but the ComboBox is filled with text 'Avalonia.Controls.ComboBox' line after line.

I am sorry I am so new to this method I even lack the language to formulate the question properly. Perhaps it is "For a list type control, how can you code it so as to have the 'source' (code supplying the list content) in one module and the 'SelectedItem' in a different module."

or maybe " how can you specify a DataContext to be the code behind module for any given xaml"

Thanks for your patience JC

Adding Full code for a cut down version. Any auto generated code has been left in

MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:Colour_Font_experiment3v1_snip.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:views="clr-namespace:Colour_Font_experiment3v1_snip.Views"
        mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450"
        Width="900" Height="450"
        x:Class="Colour_Font_experiment3v1_snip.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="Colour_Font_experiment3v1_snip">

    <Design.DataContext>
        <!-- This only sets the DataContext for the previewer in an IDE,
             to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <StackPanel Orientation="Horizontal" Spacing="10" Background="#E6F5FC" >
        <Border HorizontalAlignment="Left"
                BorderBrush="Gray" BorderThickness="1"
                CornerRadius="5" Padding="15 3">
            <StackPanel Orientation="Vertical" Spacing="10">
                <TextBlock Text="{Binding DummyPlaceHolder}" HorizontalAlignment="Left" VerticalAlignment="Center" Width="200" FontStyle="Italic"/>

                <Grid ColumnDefinitions="*, 3*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"  Margin="4"
                      ShowGridLines ="True">
                    <Label Content="" Grid.Column="0" Grid.Row="0" Height="1" />
                    <Label Content="Theme :" Grid.Column="0" Grid.Row="1"  Height="50"/>
                    <Label Content="Font :" Grid.Column="0" Grid.Row="2" Height="50" />
                    <Label Content="Start Folder:" Grid.Column="0" Grid.Row="3" Height="50" />
                    <Label Content="" Grid.Column="0" Grid.Row="4" Height="50" />
            
                    
                    <!-- see MainWindow.axaml.cs for ItemsSource ==> ThemesCombobox.ItemsSource = Themes; -->
                    <ComboBox x:Name="ThemesComboBox"   Grid.Column="1"  Grid.Row="1"
                              SelectedItem="{Binding SelectedTheme}" 
                              Width="150" Height="40"  >
                    </ComboBox>
                    
                    <!-- and https://docs.avaloniaui.net/docs/reference/controls/combobox for use of textblock within ComboBox -->
                    <ComboBox x:Name="FontComboBox"   Grid.Column="1"  Grid.Row="2"
                              MaxDropDownHeight="300" Width="150" Height="40" 
                              SelectedItem="{ Binding SelectedFont}" >
                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="xxxx" />
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>
            
                    <TextBox Grid.Column="1"  Grid.Row="3"
                             Text="{Binding StartFolder}" 
                             Padding="10,8" 
                             Width="400" Height="40" />
                    
                    <Button  Grid.Column="1"  Grid.Row="4"
                             Command="{Binding SaveButtonClick}" 
                             Content="Set / Save" 
                             Height="40" Width="100" />
                </Grid>

            </StackPanel>
        </Border>
      </StackPanel>
</Window>

MainWindow.axaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Fonts.Inter;
using Avalonia.Media;
using Avalonia.Media.Fonts;


namespace Colour_Font_experiment3v1_snip.Views;

public partial class MainWindow : Window
{
    private string[] Themes = new string[] { "Dark", "Light" };
    private List<string> lFonts = new List<string>();
    public MainWindow()
    {
        InitializeComponent();
        ThemesComboBox.ItemsSource = Themes;
        IFontCollection allFonts = FontManager.Current.SystemFonts;
        foreach (var font in allFonts)
        {
            Console.WriteLine(font.Name);
            lFonts.Add(font.Name);
        }
        Console.WriteLine($"{lFonts.Count} font Names loaded");
        FontComboBox.ItemsSource = lFonts;
        
        FontComboBox.SelectedIndex = 0;
    }
}

MainWindowViewModel

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive;
using Avalonia;
using Avalonia.Interactivity;
using ReactiveUI;
using Romzetron.Avalonia;

namespace Colour_Font_experiment3v1_snip.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
    
    // *****************************************************************
    private string _dummyPlaceHolder  = "Under Construction!";
    public string DummyPlaceHolder
    {
        get => _dummyPlaceHolder;
        set => this.RaiseAndSetIfChanged(ref _dummyPlaceHolder, value);
    }
    public ReactiveCommand<Unit, Unit> SaveButtonClick { get; }

    private string _selectedTheme = "Light";
    public string SelectedTheme
    {
        get => _selectedTheme;
        set => this.RaiseAndSetIfChanged(ref _selectedTheme, value);
    }

    private string _selectedFont; 
    public string SelectedFont
    {
        get => _selectedFont;
        set => this.RaiseAndSetIfChanged(ref _selectedFont, value);
    }
    
    private string _startFolder = @"M:\Museum\ManagedCatalog";
    public string StartFolder
    {
        get => _startFolder;
        set => this.RaiseAndSetIfChanged(ref _startFolder, value);
    }

    
    // **********************************************************
    // Constructor
    public MainWindowViewModel()
    {
        SaveButtonClick = ReactiveCommand.Create(PerformSaveAction);
        DummyPlaceHolder = "Current Colour Base is : 'redacted' " ;
    }

    private void PerformSaveAction()
    {
        Console.WriteLine("The \'Set / Save\' button was pressed.");
    }

    //*******************************************
    private ColorTheme getCurrentTheme()
    {
        return GetRomzetronAvaloniaTheme().ColorTheme;
    }
    // **********************************************************
    // JC added from https://github.com/Romzetron/Romzetron.Avalonia?tab=readme-ov-file
    private static RomzetronTheme? GetRomzetronAvaloniaTheme()
    {
        // Get a reference to the current application.
        if (Application.Current is not App app)
            return null;

        // Loop through the styles in the application.
        foreach (var style in app.Styles)
        {
            // Return RomzetronTheme if it is found.
            if (style is RomzetronTheme theme)
                return theme;
        }
        return null;
    }
 
}

And just to refocus. The issue is to get the xaml line

   <TextBlock Text="xxxx" />

actually connected to lFonts in the code behind

Thanks again

JC


Solution

  • your Item source in the ComboBox is list of strings not list of objects so you should bind to it directly as following

    <TextBlock Text="{Binding .}" />