0

I am building a desktop app using WPF, I need to be able to add views dynamically in a way that will look organized.

I have created an ObservableCollection, and set itemsControl.ItemsSource to be that collection.

I am able to add views, it just looks awful.

All I know about the amount of views that can be added, is that there can be a maximum of 16 views.

I thought about creating a dynamic grid, that will change according to the amount of views available, as in the following image

following

So I tried going with the dynamic-grid idea, I am creating a new grid each time a view is to be added/removed (sounds bad), and by the size of the views collection I know how to split the grid correctly.

Then I loop over all of the existing views in the collection, and place them on the grid by the right logic. I kind of made a real mess here, so if I'm totally in the wrong direction please just let me know.

Here is the code:

public partial class DynamicTargetView : UserControl
{
    private ObservableCollection<TargetView> views = new ObservableCollection<TargetView>();
    public Grid grid;

    public DynamicTargetView()
    {
        InitializeComponent();
        SettingsBar.onViewChange += addOrRemoveTargetsFromPanel;

    }


    public void addOrRemoveTargetsFromPanel(Object sender, WeiTargetGui.EventArgs.AddOrRemoveViewEventArgs e)
    {
        if (e.isShown)
        {
            addTargetToPanel(e.id);
        }
        else removeTargetFromPanel(e.id);
    }

    public void addTargetToPanel(string id)
    {
        views.Add(new TargetView(Int32.Parse(id)));

        ArrangeGrid();
    }

    public void removeTargetFromPanel(string id)
    {
        foreach (TargetView v in views)
        {
            if (v.id == Int32.Parse(id))
                views.Remove(v);
        }
        ArrangeGrid();
    }
    public void ArrangeGrid()
    {
        int NumOfViews = views.Count();
        grid = new Grid();
        grid.Children.Clear();

        ColumnDefinition gridCol1;
        ColumnDefinition gridCol2;
        ColumnDefinition gridCol3;
        ColumnDefinition gridCol4;
        RowDefinition gridRow1;
        RowDefinition gridRow2;
        RowDefinition gridRow3;
        RowDefinition gridRow4;

        if (NumOfViews == 1)
        {
            addtoGrid(0, 0);
        }
        else if (NumOfViews == 2)
        {
            gridCol1 = new ColumnDefinition();
            gridCol2 = new ColumnDefinition();
            grid.ColumnDefinitions.Add(gridCol1);
            grid.ColumnDefinitions.Add(gridCol2);
            addtoGrid(2, 0);
        }
        else if (NumOfViews < 5)
        {
            gridCol1 = new ColumnDefinition();
            gridCol2 = new ColumnDefinition();
            gridRow1 = new RowDefinition();
            gridRow2 = new RowDefinition();

            grid.ColumnDefinitions.Add(gridCol1);
            grid.ColumnDefinitions.Add(gridCol2);
            grid.RowDefinitions.Add(gridRow1);
            grid.RowDefinitions.Add(gridRow2);
            addtoGrid(2, 2);
        }
        else if (NumOfViews < 10)
        {
            gridCol1 = new ColumnDefinition();
            gridCol2 = new ColumnDefinition();
            gridCol3 = new ColumnDefinition();
            gridRow1 = new RowDefinition();
            gridRow2 = new RowDefinition();
            gridRow3 = new RowDefinition();
            grid.ColumnDefinitions.Add(gridCol1);
            grid.ColumnDefinitions.Add(gridCol2);
            grid.ColumnDefinitions.Add(gridCol3);
            grid.RowDefinitions.Add(gridRow1);
            grid.RowDefinitions.Add(gridRow2);
            grid.RowDefinitions.Add(gridRow3);

            addtoGrid(3, 3);
        }
        else if (NumOfViews < 17)
        {
            gridCol1 = new ColumnDefinition();
            gridCol2 = new ColumnDefinition();
            gridCol3 = new ColumnDefinition();
            gridCol4 = new ColumnDefinition();
            gridRow1 = new RowDefinition();
            gridRow2 = new RowDefinition();
            gridRow3 = new RowDefinition();
            gridRow4 = new RowDefinition();

            grid.ColumnDefinitions.Add(gridCol1);
            grid.ColumnDefinitions.Add(gridCol2);
            grid.ColumnDefinitions.Add(gridCol3);
            grid.ColumnDefinitions.Add(gridCol4);
            grid.RowDefinitions.Add(gridRow1);
            grid.RowDefinitions.Add(gridRow2);
            grid.RowDefinitions.Add(gridRow3);
            grid.RowDefinitions.Add(gridRow4);

            addtoGrid(4, 4);
        }


    }

    public void addtoGrid(int cols, int rows)
    {
        int row = 0;
        int column = 0;

        foreach (var view in views)
        {
            if (cols == 0 && rows == 0)
            {
                grid.Children.Add(view);
                break;
            }
            if (cols == 2 && rows == 0)
            {
                Grid.SetColumn(view, column);
                column++;
                grid.Children.Add(view);
            }
            else
            {
                if (column != cols)
                {
                    Grid.SetColumn(view, column);
                    Grid.SetRow(view, row);
                    grid.Children.Add(view);
                }
                if (column < cols)
                    column++;
                else
                {
                    column = 0;
                    row++;
                }

            }

        }
       this.Content = grid;
    }

}

TargetView is a user-control that represents a target with some data about that target.

So here are the problems with the code -

1) When adding more then one component to the views collection I get the following exception:

System.InvalidOperationException: 'Specified element is already the logical child of another element. Disconnect it first.'

I have added the grid.Children.Clear() - with no help.

2) Final problem - code looks really bad, I just want to make it work so I could learn and implement it better next time.

Help will be appreciated, thanks a lot.

RanAB
  • 397
  • 1
  • 4
  • 16

2 Answers2

1

You're going about this completely wrong. You'll save yourself a ton of headache if you just do things properly and use data binding, if not full-blown MVVM.

Whenever you want to display a list of "things" (views or anything else) in WPF you should use an ItemsSource bound to a collection of items containing the logic for the views you want to create. When you do this you'll see what looks like a regular ListBox, with each element being a line of text containing the class name. The trick is to then use DataTemplates to specify what type of view to create for each of your back-end types, i.e.:

<DataTemplate TargetType="{x:Type local:MyViewModelTypeA}">
    <view:MyViewTypeA />
</DataTemplate>

<DataTemplate TargetType="{x:Type local:MyViewModelTypeB}">
    <view:MyViewTypeB />
</DataTemplate>

Do this and your views will now look correct, but they'll still be in a vertical StackPanel. To change that you can replace the default ItemsPanel:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapPanel />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

If you want something a bit more "grid-like" then you could also use a UniformGrid, and bind its NumColumns to a property in your parent class that calculates this value at runtime.

There are lots of variations on this and many different parameters to tweak, but that's the general idea. The key to WPF is doing as much logic in your DataContext classes as possible, and bind your front end to it with the most light-weight binding possible.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • So if I understood your answer correctly, in my case `MyViewModelTypeA` will be the observable collection? And the `MyViewTypeA ` will be TargetView (a user control)? I'm asking because all of the views are from the same type. So in fact I will need to "declare" this only once in my xaml. Correct? – RanAB Jul 24 '19 at 12:49
  • MyViewTypeA is your target view but MyViewModelTypeA is a class you create that contains the properties that each of your MyViewTypeA instances need to display. Your parent class then maintains a property `ObservableCollection MyItems {get; set;}` and your ItemsControls binds to it with `ItemsSource="{Binding MyItems}"`. – Mark Feldman Jul 24 '19 at 21:20
  • [This link](https://www.wpf-tutorial.com/list-controls/itemscontrol/) shows what I'm talking about in a bit more detail, in fact it might even be more suitable in your case since you only have a single view type (the only reason I suggested DataTemplates is because that's how you do multiple types). He assigns ItemsSource directly (instead of a binding declaration), but it's the same principle...his `TodoItem` class is the "view model" that the views all bind to. – Mark Feldman Jul 24 '19 at 21:31
  • Thanks alot! Got it. – RanAB Jul 25 '19 at 07:47
0

If elements have the same size use UniformGrid instead of Grid, this let you avoid unnecessary calculations

wpfPanels

EmerG
  • 1,046
  • 8
  • 13