7

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

Luke Dinkler
  • 731
  • 5
  • 16
  • 36
  • Are you looking for something akin to **VirtualizingStackPanel** – user6144226 Jan 17 '18 at 23:16
  • @user6144226 Maybe...I've never used one before. – Luke Dinkler Jan 17 '18 at 23:19
  • Why do you need it to be programmatically? – Wouter Jan 17 '18 at 23:23
  • @Wouter It's complicated. In few words, the entire UI is dynamic in a sense (it can be moved, changed, etc.). – Luke Dinkler Jan 17 '18 at 23:25
  • Ok, mostly you can do the xaml stuff easier in xaml (I would call that the best practice) than in code but you should be able to do it. I never used it but there are options to load xaml with XamlServices or XamlSerialzier to save and load e.g. the itemtemplate. – Wouter Jan 17 '18 at 23:41
  • @Wouter There are things you can do in _XAML_ which you cannot do in _C#_, e.g. when defining a `ControlTemplate` you can easily set a CLR property in _XAML_, but it's not possible with `FrameworkElementFactory`. – Grx70 Feb 12 '18 at 07:10
  • @Grx70 Not even with reflection? – Wouter Feb 12 '18 at 07:54
  • Sounds like you want a `WrapPanel`: it's like a `StackPanel`, but when it reaches the end of the row/column (depending on `Orientation`), it wraps around to the _next_ row/column. – Mike Strobel Feb 12 '18 at 15:02
  • 2
    IMHO, doing WPF w/o DataBinding is theoratically possible, but practically impossible. If you don't want databinding (whatever that could mean?), you shouldn't use WPF. DataBinding is a vital part of WPF, not an addon. – Simon Mourier Feb 13 '18 at 07:28
  • @SimonMourier DataBinding, in that I don't want to "bind" specific data to the elements of the UI. I should be able to add/remove/change the data via iterations in C# code. – Luke Dinkler Feb 13 '18 at 14:17
  • Luke, yes that can be done with databinding as long as you properly notify the bindings of the change. Do your classes implement inotifypropertychanged and your classes are kept inside an observablecollection container? – Kevin Cook Feb 13 '18 at 15:09
  • @KevinCook I was hoping to not complicate it that far. I just need to create a scrollable list of stackpanels/grids/groups of elements via C# code. It shouldn't be much more complicated than adding any other UIElement (in code-behind). I shouldn't need any data bindings to accomplish this if I'm going to add them via iteration. – Luke Dinkler Feb 13 '18 at 15:35
  • @Luke I dont think you understand what Data binding really does. IT does not complicate things. IT makes them easier to read and understand. And why do you want a scrollviewer for each column ? Create a Viewmodel, in that an observablecollection of movie items. In xaml create an itemscontrol. You can assign every – Mightee Feb 17 '18 at 05:53
  • @Mightee Yeah ok, buddy... – Luke Dinkler Feb 17 '18 at 14:13
  • @Luke Dinkler ... we are all here trying to make you understand you are making a mistake thinking databinding `defeats the purpose of the dynamic nature of runtime.` any property you want to set in code behind, can be used as data binding property, including events (on click, on drag anything you want). you should actually use MVVM patterns and data binding. – Mightee Feb 17 '18 at 14:31
  • @Mightee Ok, but you didn't even bother to finish your sentence on your first comment. Data bindings do, in a sense, defeat the purpose of creating controls at runtime. At runtime, you can create any number of controls and set their properties as a result of calculation (something you cannot do in XAML). I don't believe that data bindings are useless in anyway, but what I'm doing is creating the whole UI at runtime, not just a control. Therefore, I would prefer to NOT use implementations like `FrameworkFactory` or `object.SetBinding()` when I could just set the properties directly. – Luke Dinkler Feb 17 '18 at 14:37
  • didnt realize that. if you make a scrollviewer and set its itemssource property to an ObservableCollection you would get the exact same behaviour with much less complicated and better structured code. you should re think about creating the whole UI at runtime. IT IS possible to do all of that ugly code behind with data binding. – Mightee Feb 17 '18 at 14:39

1 Answers1

3

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);
        }
    }
}
Roger Leblanc
  • 1,543
  • 1
  • 10
  • 9