1

In my Avalonia app I have a user control (UC) that has some databound controls and a StackPanel that has controls added to it dynamically from code-behind. This UC is then dynamically added to an outer user control of another type. I want to create an instance of the UC in code and add to it--among other controls--an ItemsControl and have the ItemsSource of that ItemsControl bind to an ObservableCollection property of the viewmodel class that becomes the UC's DataContext.

In the sample app I created to isolate the problem, I have a bookshelf UC that displays the four books collected in the viewmodel. The UC already has a TextBlock bound to the VM's category property, with a StackPanel for additional controls like the ItemsControl for the books and a TextBlock for the shelf itself. The UC gets dynamically added to the main window along with a TextBlock representing mounting brackets underneath. It should end up looking like this:

enter image description here

...as though an ItemsControl defined in XAML was:

    <ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding Books}" Margin="0,25,0,0">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <Border BorderBrush="Brown" BorderThickness="2">
            <StackPanel>
              <TextBlock Text="{Binding Title}" FontWeight="Bold" Background="#BC8F6F" Foreground="White" />
              <TextBlock>
                <TextBlock.Text>
                  <MultiBinding StringFormat="{}by {0}, {1}">
                    <Binding Path="Author" />
                    <Binding Path="YearPublished" />
                  </MultiBinding>
                </TextBlock.Text>
              </TextBlock>
            </StackPanel>
          </Border>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>

But every set of book controls for the ItemTemplate instead shows its type name:

enter image description here

BookVM.cs:

  public partial class BookVM : ViewModelBase {
    [ObservableProperty]
    private string _title = "Read This";
    [ObservableProperty]
    private string _author = "A. Writer";
    [ObservableProperty]
    private int _yearPublished = 2025;
  }

BookshelfVM.cs:

  public partial class BookshelfVM : ObservableObject {
    [ObservableProperty]
    private string _category;
    public ObservableCollection<BookVM> Books { get; set; } = [];

    public BookshelfVM() {
      Category = "Science Fiction";
      Books.Add(new BookVM { Title = "Fahrenheit 451", Author = "Ray Bradbury", YearPublished = 1953 });
      Books.Add(new BookVM { Title = "Dune", Author = "Frank Herbert", YearPublished = 1965 });
      Books.Add(new BookVM { Title = "Neuromancer", Author = "William Gibson", YearPublished = 1984 });
      Books.Add(new BookVM { Title = "Ender’s Game", Author = "Orson Scott Card", YearPublished = 1985 });
    }
  }

BookshelfUC.axaml:

  <Grid>
    <TextBlock Text="{Binding Category}" FontSize="18" FontWeight="Bold" />
    <StackPanel x:Name="stpBooksPlusShelf" Margin="0,25,0,0" />
  </Grid>

MainView.axaml:

  <DockPanel HorizontalAlignment="Left" VerticalAlignment="Stretch">
    <TextBlock DockPanel.Dock="Top" Text="This should show a stack of books." Margin="0,0,0,10" />
    <StackPanel x:Name="stpDynamicControls" />
  </DockPanel>

MainView.axaml.cs:

public partial class MainView : UserControl {
  public MainView() {
    InitializeComponent();
    var dynamicBookshelf = new BookshelfUC();// { DataContext = new BookshelfVM() };
    var itemsCtlToAdd = new ItemsControl {
      [!ItemsControl.ItemsSourceProperty] = new Binding("Books")
    };
    var dataTmplt = new StackPanel();
    var borderedStack = new StackPanel();
    var bookBorder = new Border {
      BorderBrush = new SolidColorBrush(Colors.Brown),
      BorderThickness = new Avalonia.Thickness(2)
    };
    var title = new TextBlock {
      Background = new SolidColorBrush(Color.Parse("#BC8F6F")),
      Foreground = new SolidColorBrush(Colors.White),
      [!TextBlock.TextProperty] = new Binding("Title")
    };
    var authorAndYear = new TextBlock {
      [!TextBlock.TextProperty] = new Binding("YearPublished")
    };
    var shelfAfterBooks = new TextBlock {
      Background = new SolidColorBrush(Colors.Black),
      Foreground = new SolidColorBrush(Colors.White),
      Text = "Shelf on which the stack of book sits"
    };
    var bracket = new TextBlock {
      Background = new SolidColorBrush(Colors.LightGray),
      Text = "Mounting brackets"
    };
    borderedStack.Children.Add(title);
    borderedStack.Children.Add(authorAndYear);
    bookBorder.Child = borderedStack;
    dataTmplt.Children.Add(bookBorder);
    itemsCtlToAdd.ItemTemplate = new FuncDataTemplate<string>((item, _) =>
    {
      return dataTmplt;
    });

    dynamicBookshelf.stpBooksPlusShelf.Children.Add(itemsCtlToAdd);
    dynamicBookshelf.stpBooksPlusShelf.Children.Add(shelfAfterBooks);
    dynamicBookshelf.DataContext = new BookshelfVM();
    stpDynamicControls.Children.Add(dynamicBookshelf);
    stpDynamicControls.Children.Add(bracket);
  }
}

Where has my syntax gone wrong? Thanks...

1 Answer 1

1

the problem here that you didnot provide a View for the type BookVM you provied a view for a string instade here is how to fix it

public partial class MainView : UserControl
{
    public MainView()
    {
        InitializeComponent();
        var dynamicBookshelf = new BookshelfUC();// { DataContext = new BookshelfVM() };
        var itemsCtlToAdd = new ItemsControl
        {
            [!ItemsControl.ItemsSourceProperty] = new Binding("Books")
        };

        // the following line will create the DataTemplate for each BookVM item
        itemsCtlToAdd.ItemTemplate = new FuncDataTemplate<BookVM>((item, _) =>
        {
            var dataTmplt = new StackPanel();
            var borderedStack = new StackPanel();
            var bookBorder = new Border
            {
                BorderBrush = new SolidColorBrush(Colors.Brown),
                BorderThickness = new Avalonia.Thickness(2)
            };
            var title = new TextBlock
            {
                Background = new SolidColorBrush(Color.Parse("#BC8F6F")),
                Foreground = new SolidColorBrush(Colors.White),
                [!TextBlock.TextProperty] = new Binding("Title") // instead of binding u can use of item.Title
            };
            var authorAndYear = new TextBlock
            {
                Text = $"by {item.Author}, {item.YearPublished}"  // i show how to use item parameter here
            };            
            
            borderedStack.Children.Add(title);
            borderedStack.Children.Add(authorAndYear);
            bookBorder.Child = borderedStack;
            dataTmplt.Children.Add(bookBorder);
            return dataTmplt;
        });

        var shelfAfterBooks = new TextBlock
        {
            Background = new SolidColorBrush(Colors.Black),
            Foreground = new SolidColorBrush(Colors.White),
            Text = "Shelf on which the stack of book sits"
        };
        var bracket = new TextBlock
        {
            Background = new SolidColorBrush(Colors.LightGray),
            Text = "Mounting brackets"
        };
        dynamicBookshelf.stpBooksPlusShelf.Children.Add(itemsCtlToAdd);
        dynamicBookshelf.stpBooksPlusShelf.Children.Add(shelfAfterBooks);
        dynamicBookshelf.DataContext = new BookshelfVM();
        stpDynamicControls.Children.Add(dynamicBookshelf);
        stpDynamicControls.Children.Add(bracket);
    }
}

and this aproach in my openion is very difficult and messy ...

Alterntive Approch

there is an easyer and cleaner approch, you can provide your view in XAML Formate

BookView.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="using:AvaloniaApplication29.ViewModels"
             x:DataType="vm:BookVM"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaApplication29.BookView">
  <Border BorderBrush="brown" BorderThickness="2">
      <StackPanel>        
            <TextBlock Text="{Binding Title}" 
                    Foreground="white" 
                    Background="#BC8F6F" />
             <StackPanel Orientation="Horizontal" Background="#eee">
                 <StackPanel.Styles>
                     <Style Selector="Label">
                         <Setter Property="FontSize" Value="12"/>
                         <Setter Property="Foreground" Value="#333"/>
                     </Style>
              </StackPanel.Styles>

                <Label Content="by "/>
                <Label Content="{Binding Author}" />
                <Label Content=", " /> 
                <Label Content="{Binding YearPublished}"/>
             </StackPanel>

      </StackPanel>
  </Border>
</UserControl>

BookshelfUC.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="using:AvaloniaApplication29.ViewModels" 
             x:DataType="vm:BookshelfVM"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaApplication29.BookshelfUC">
  <Grid>
    <TextBlock Text="{Binding Category}" FontSize="18" FontWeight="Bold" />
    <ItemsControl ItemsSource="{Binding Books}" Margin="0,25,0,0"/>
  </Grid>
</UserControl>

and because you don't follow Naming convintion and ViewLocator pattern you should tell your app how to connect between your view and your ViewModle ... (i.e how to chooes the correct view for Randring a ViewModel)

in App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="AvaloniaApplication29.App"
             xmlns:vm="using:AvaloniaApplication29.ViewModels"
             xmlns:views="using:AvaloniaApplication29"
             RequestedThemeVariant="Default">
             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

    <Application.DataTemplates > 
     <DataTemplate DataType="vm:BookshelfVM"  >
        <views:BookshelfUC/>
     </DataTemplate>
     <DataTemplate DataType="vm:BookVM" >
        <views:BookView/>
     </DataTemplate>
    </Application.DataTemplates>
    
    <Application.Styles>
        <FluentTheme />
    </Application.Styles>
</Application>

now let't try it add some shelves in your MainViewModel.cs

public partial class MainViewModel : ViewModelBase
{ 
    public ObservableCollection<BookshelfVM> Bookshelves { get; } = [
            new BookshelfVM
            {
                Category = "Science Fiction",
                Books = new ObservableCollection<BookVM>
                {
                    new BookVM { Title = "Fahrenheit 451", Author = "Ray Bradbury", YearPublished = 1953 },
                    new BookVM { Title = "Dune", Author = "Frank Herbert", YearPublished = 1965 },
                    new BookVM { Title = "Neuromancer", Author = "William Gibson", YearPublished = 1984 },
                    new BookVM { Title = "Ender’s Game", Author = "Orson Scott Card", YearPublished = 1985 }
                }
            },
            new BookshelfVM
            {
                Category = "Fantasy",
                Books = new ObservableCollection<BookVM>
                {
                    new BookVM { Title = "The Hobbit", Author = "J.R.R. Tolkien", YearPublished = 1937 },
                    new BookVM { Title = "A Game of Thrones", Author = "George R.R. Martin", YearPublished = 1996 },
                    new BookVM { Title = "The Name of the Wind", Author = "Patrick Rothfuss", YearPublished = 2007 },
                    new BookVM { Title = "Mistborn: The Final Empire", Author = "Brandon Sanderson", YearPublished = 2006 }
                }
            }
        ];
}

and add your view as following

MainView.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:AvaloniaApplication29.ViewModels"
             xmlns:views="clr-namespace:AvaloniaApplication29"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaApplication29.Views.MainView"
             x:DataType="vm:MainViewModel">
  <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:MainViewModel />
  </Design.DataContext>

  <DockPanel HorizontalAlignment="Left" VerticalAlignment="Stretch">
    <TextBlock DockPanel.Dock="Top" Text="This should show a stack of books." Margin="0,0,0,10" />
    <ItemsControl ItemsSource="{Binding Bookshelves}"/>     
  </DockPanel>

</UserControl>

and then it will work as charm here is the result after running

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Ah yes--that <string> is a holdover from the "real" app from which this problem was isolated. I'm ultimately trying to generate user controls dynamically based on loaded JSON data. The data provides the property names, but not class types, so I may have to rethink my strategy. I agree it's messy, but my manager wants to avoid having a solution with over 300 XAML files. Thank you for your thoughtful answer!
Writing Views in Axaml is always the easiest way. By the way, you write the same amount of code in code-behind, but it wasn't easy to follow. This is an isolated one—what about the whole app? It would be chaotic then!! ....you can make a post in code review site with the JSON format/Fake Data with your implementation, and you will get ideas to improve and implement it in a maintainable, easy way

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.