0
votes

To make my canvas automatically adjust its size so that my ScrollViewer works properly, I've made a class that derives from Canvas which then overrides the MeasureOverride method to make the canvas adjust its size based on the UIElements present on the canvas.

public class DesignerCanvas : Canvas
{
    protected override Size MeasureOverride(Size constraint)
    {
        Size size = new Size();
        foreach (UIElement element in Children)
        {
            double left = Canvas.GetLeft(element);
            double top = Canvas.GetTop(element);
            left = double.IsNaN(left) ? 0 : left;
            top = double.IsNaN(top) ? 0 : top;

            element.Measure(constraint);

            Size desiredSize = element.DesiredSize;
            if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height))
            {
                size.Width = Math.Max(size.Width, left + desiredSize.Width);
                size.Height = Math.Max(size.Height, top + desiredSize.Height);
            }
        }

        // add some extra margin
        size.Width += 10;
        size.Height += 10;
        base.InvalidateMeasure();
        return size;
    }
}

XAML:

<ScrollViewer HorizontalScrollBarVisibility="Visible">
        <local:DesignerCanvas x:Name="designerCanvas"  AllowDrop="True">
            <ItemsControl Name="Shapes">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </local:DesignerCanvas>
    </ScrollViewer>

And the scrollviewer works properly whenever I add/remove graphics directly to the DesignerCanvas! except I'm not. I'm taking the MVVM approach so I'm databinding my canvas to the ObservableCollection

    private ObservableCollection<Shape> shapes = new ObservableCollection<Shape>();
    public ObservableCollection<Shape> Shapes 
    {
        get { return shapes; }
        set { shapes = value; }
    }

The shapes show up on the canvas whenever the items are added to the observablecollection. However, the canvas doesn't resize itself properly which causes my scrollviewer to not work anymore. This is expected since I am not directly manipulating the DesignerCanvas, so MeasureOverride is never called.

I could call InvalidateMeasure on DesignerCanvas, but that requires my viewmodel to have knowledge about the view. How do I solve this problem without breaking MVVM?

Edit: I'd also like to hear any recommendations if it's actually worth keeping this pattern for problems like this. Lately, I feel like MVVM is giving me more problems than solutions.

Edit2: Ohh.. Ok, this problem was a lot more convoluted than I thought. From Florian Gl's suggestion, I went ahead and created an event in my viewmodel, then handled that event in my view to call designerCanvas.InvalidateMeasure().

This calls my MeasureOverride method in DesignerCanvas. Except it doesn't adjust my canvas size properly, so scrollviewer still doesn't work. I've noticed that I'm not adding shapes directly to the children of the DesignerCanvas but rather, to the observablecollection which is databound to the Canvas. Hence, there are no UIElement to loop through in my foreach statement

    protected override Size MeasureOverride(Size constraint)
    {
        Size size = new Size();

        ItemsControl itemsControl = Children.OfType<ItemsControl>().First();


        foreach (UIElement element in Children) //No UIElements inside Children
        {
            double left = Canvas.GetLeft(element);
            ...

Now I'm really stuck. How can I circumvent this issue?

1
Why dont you call designerCanvas.InvalidateMeasure() in your xaml.cs? Would this break MVVM?Florian Gl
@FlorianGl Absolutely not, but the question is then how do I know when to call the method? In my view, I have no knowledge of the observablecollection in my Viewmodel so I can't tell when items are getting added/removed from the collection. The only viable option I can think of is to attach a periodic timer or something and call the invalidate method but this is a really dirty solution.l46kok
You could add an event in your ViewModel (e.g. MyCollectionChanged) and let it fire whenever your collection gets changed. In your xaml.cs you call designerCanvas.InvalidateMeasure() when the event fires.Florian Gl

1 Answers

1
votes

My Event-Idea:

  1. Create your own event in ViewModel:

    public delegate void MyCollectionChangedEventArgs();
    public event MyCollectionChangedEventArgs MyCollectionChanged;
    
  2. Call Event in ViewModel:

    if (MyCollectionChanged != null)
    {
        MyCollectionChanged();
    }
    
  3. Let your View know about the event:

        MainWindowViewModel vm = new MainWindowViewModel();
        vm.MyCollectionChanged += new MainWindowViewModel.MyCollectionChangedEventArgs(MainWindowViewModel_MyCollectionChanged);
    

    or

        MainWindowViewModel vm = (MainWindowViewModel)DataContext;
        vm.MyCollectionChanged += new MainWindowViewModel.MyCollectionChangedEventArgs(MainWindowViewModel_MyCollectionChanged);
    
  4. Call Method:

    void MainWindowViewModel_MyCollectionChanged()
    {
        designerCanvas.InvalidateMeasure()
    }
    

EDIT: I dont know if my following suggestion is working in the MeasureOverride-Method, but you could try:

    (MainWindowViewModel) mvm = (MainWindowViewModel)DataContext; 

    foreach (UIElement element in mvm.Shapes)
    { ... }