Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to programmatically create WPF scrollable StackPanel list in C#

My Situation:

I'm developing a C# WPF Application (on Windows) where I need to dynamically create a lot of my controls at runtime. Because of the nature of the application, I'm not able to use standard XAML (with Templates) for many aspects of my WPF windows. This is a very unique case, and no, I'm not going to reconsider the format of my application.

What I want to accomplish:

I would like to programmatically create a control that displays a scrollable list of StackPanels (or any other effecient control group) which, for one use case, will each consist of an Image control (picture) on top of a TextBlock control (title/caption):

  • I would prefer to do all this without any data bindings (See below for reasoning). Because the items are being defined at runtime, I should be able to do this without them via iteration.
  • The control/viewer should be able to have multiple columns/rows, so it's not one dimensional (like a typical ListBox control).
  • It should also interchangeable so that you can modify (add, remove, etc.) the items in the control.

I've included a picture (below) to give you an example of a possible use case.

In the past, I have been able to accomplish all this by using a ListView with an ItemTemplate (wrapped in a ScrollViewer) using XAML. However, doing this entirely with C# code makes it a bit more difficult. I've recently made ControlTemplates in plain c# code (with FrameworkElementFactorys. It can get a bit complicated, and I'm not sure it's really the best practice. Should I try to go the same route (using a ListView with a template)? If so, how? Or is there a simpler, more elegant option to implement with C# code?

enter image description here

Edit: I would really prefer not to use any data bindings. I just want to create a (scrollable) 'list' of StackPanels that I can easily modify/tweak. Using data bindings feels like a backwards implementation and defeats the purpose of the dynamic nature of runtime.

Edit 2 (1/25/2018): Not much response. I simply need a uniform, scrollable list of stackpanels. I can tweak it to suit my needs, but it needs to be all in C# (code-behind). If anyone needs more information/clarification, please let me know. Thanks.

LINK TO XAML POST

like image 904
Luke Dinkler Avatar asked Jan 17 '18 23:01

Luke Dinkler


1 Answers

Here's a way to do it in code using a ListBox with UniformGrid as ItemsPanelTemplate. Alternatively, you can only use a UniformGrid and put it inside a ScrollViewer, but as the ListBox already handles selection and all that stuff, you probably better stick with that one. This code will automatically adjust the number of items in a row depending on the available width.

MoviePresenter.cs :

public class MoviePresenter : ListBox
{
    public MoviePresenter()
    {
        FrameworkElementFactory factory = new FrameworkElementFactory(typeof(UniformGrid));
        factory.SetBinding(
            UniformGrid.ColumnsProperty,
            new Binding(nameof(ActualWidth))
            {
                Source = this,
                Mode = BindingMode.OneWay,
                Converter = new WidthToColumnsConverter()
                {
                    ItemMinWidth = 100
                }
            });

        ItemsPanel = new ItemsPanelTemplate()
        {
            VisualTree = factory
        };
    }
}

internal class WidthToColumnsConverter : IValueConverter
{
    public double ItemMinWidth { get; set; } = 1;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        double? actualWidth = value as double?;
        if (!actualWidth.HasValue)
            return Binding.DoNothing;

        return Math.Max(1, Math.Floor(actualWidth.Value / ItemMinWidth));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

MovieItem.cs :

public class MovieItem : Grid
{
    public MovieItem()
    {
        RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
        RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
        RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
        RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });

        Image image = new Image();
        image.Stretch = Stretch.UniformToFill;
        image.SetBinding(Image.SourceProperty, new Binding(nameof(ImageSource)) { Source = this });
        Children.Add(image);

        TextBlock title = new TextBlock();
        title.FontSize += 1;
        title.FontWeight = FontWeights.Bold;
        title.Foreground = Brushes.Beige;
        title.TextTrimming = TextTrimming.CharacterEllipsis;
        title.SetBinding(TextBlock.TextProperty, new Binding(nameof(Title)) { Source = this });
        Grid.SetRow(title, 1);
        Children.Add(title);

        TextBlock year = new TextBlock();
        year.Foreground = Brushes.LightGray;
        year.TextTrimming = TextTrimming.CharacterEllipsis;
        year.SetBinding(TextBlock.TextProperty, new Binding(nameof(Year)) { Source = this });
        Grid.SetRow(year, 2);
        Children.Add(year);

        TextBlock releaseDate = new TextBlock();
        releaseDate.Foreground = Brushes.LightGray;
        releaseDate.TextTrimming = TextTrimming.CharacterEllipsis;
        releaseDate.SetBinding(TextBlock.TextProperty, new Binding(nameof(ReleaseDate)) { Source = this });
        Grid.SetRow(releaseDate, 3);
        Children.Add(releaseDate);
    }

    public static readonly DependencyProperty ImageSourceProperty =
        DependencyProperty.Register("ImageSource", typeof(string), typeof(MovieItem), new PropertyMetadata(null));

    public static readonly DependencyProperty TitleProperty =
        DependencyProperty.Register("Title", typeof(string), typeof(MovieItem), new PropertyMetadata(null));

    public static readonly DependencyProperty YearProperty =
        DependencyProperty.Register("Year", typeof(string), typeof(MovieItem), new PropertyMetadata(null));

    public static readonly DependencyProperty ReleaseDateProperty =
        DependencyProperty.Register("ReleaseDate", typeof(string), typeof(MovieItem), new PropertyMetadata(null));

    public string ImageSource
    {
        get { return (string)GetValue(ImageSourceProperty); }
        set { SetValue(ImageSourceProperty, value); }
    }

    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public string Year
    {
        get { return (string)GetValue(YearProperty); }
        set { SetValue(YearProperty, value); }
    }

    public string ReleaseDate
    {
        get { return (string)GetValue(ReleaseDateProperty); }
        set { SetValue(ReleaseDateProperty, value); }
    }
}

MainWindow.xaml :

<Grid>
    <local:MoviePresenter x:Name="moviePresenter" 
                          ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
</Grid>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        for (int i = 0; i < 20; i++)
        {
            DateTime dummyDate = DateTime.Now.AddMonths(-i).AddDays(-(i * i));

            MovieItem item = new MovieItem()
            {
                ImageSource = $"http://fakeimg.pl/100x200/?text=Image_{i}",
                Title = $"Dummy movie {i}",
                Year = $"{dummyDate.Year}",
                ReleaseDate = $"{dummyDate.ToLongDateString()}"
            };

            moviePresenter.Items.Add(item);
        }
    }
}
like image 71
Roger Leblanc Avatar answered Sep 23 '22 00:09

Roger Leblanc