C# WPF – Как обновить все элементы в списке на основе изменения одного из них и заставить привязки работать?

Я пытаюсь создать приложение WPF, в котором у меня есть список элементов (в данном случае стен) в DataGrid, и я хочу визуализировать геометрию элементов (стен). Я прикрепил изображение ниже.

По сути, мои объекты — это стены, имеющие высоту и длину. Стены должны быть наложены друг на друга, как на изображении ниже. (При инициализации они аккуратно складываются друг в друга только потому, что именно так я инициализировал свой список вручную.)

Главное окно

Главное окно, которое вы видите выше, состоит из 3 столбцов:

0 — в левом столбце есть панель стека, содержащая DataGrid.

1 — средний столбец имеет элемент управления ItemsControl, а для ItemsPanelTemplate установлено значение холста. Большой красный квадрат показывает контур холста в качестве временного ориентира.

2 – Правый столбец пока неактуален/не используется.

В приведенном ниже коде показано, как настраивается ItemsControl/Canvas. По сути, я визуализирую стены как границы с определенным размером и положением в зависимости от введенных данных. Я мог бы использовать и прямоугольники, но не нашел каких-либо существенных плюсов/минусов.

<ItemsControl Grid.Column = "1" 
              ItemsSource = "{Binding Walls, UpdateSourceTrigger=PropertyChanged}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Background = "IndianRed"
                    Margin = "20"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Width = "{Binding XLength}"
                    Height = "{Binding YHeight}"
                    Background = "#CCCCCC"
                    BorderBrush = "Black"
                    BorderThickness = "1">
                <TextBlock Text = "{Binding Floor}"
                           HorizontalAlignment = "Center"
                           VerticalAlignment = "Center"/>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType = "ContentPresenter">
            <Setter Property = "Canvas.Left" Value = "{Binding XPos}"/>
            <Setter Property = "Canvas.Top" Value = "{Binding YPos}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

Вот изображение моего обозревателя решений для краткого обзора:

Обозреватель решений

Стены определяются этим классом:

По сути, высота и длина — это размеры стены в реальной жизни. Я использую шкалу 50, чтобы получить XLength и YHeight, то есть размер границы, обозначающей каждую стену. Аналогично, каждая стена имеет XPos и ​​YPos с одинаковым масштабом, который определяет ее положение на холсте.

public class Wall : INotifyPropertyChanged
{
    private static int _scale = 50;

    private string _floor; //Just a string of text displayed on each Border
    private double _height; //Height of wall IRL
    private double _length; //Lenght of wall IRL
    private double _xPos; //Horizonal position on canvas (Canvas.Left)
    private double _yPos; //Vertical position on canvas (Canvas.Top)
    private double _xLength; //Width of Border on canvas (IRL Length * Scale)
    private double _yHeight; //Height of Border on canvas (IRL Height * Scale)

    public string Floor
    {
        get { return _floor; }
        set { _floor = value; }
    }

    public double Height
    {
        get { return _height; }
        set 
        { 
            _height = value;
            _yHeight = value*_scale;
            OnPropertyChanged("YHeight");
        }
    }

    public double Length
    {
        get { return _length; }
        set 
        { 
            _length = value;
            _xLength = value*_scale;
            OnPropertyChanged("XLength");
        }
    }

    public double XPos
    {
        get { return _xPos; }
        set { _xPos = value; }
    }

    public double YPos
    {
        get { return _yPos; }
        set { _yPos = value; }
    }

    public double XLength
    {
        get { return _xLength; }
        set { _xLength = Length * _scale; }
    }

    public double YHeight
    {
        get { return _yHeight; }
        set { _yHeight = Height * _scale; }
    }
    
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

Переменные Стены описаны на этом рисунке:

Описание XLength, YHeight, XPos, YPos

В свойствах XLength и YHeight я успешно применил несколько простых формул, чтобы длина и высота соответствовали друг другу. Например, когда я меняю несколько значений в DataGrid, я могу получить что-то вроде этого:

MainWindow при изменении значений в DataGrid

Однако мне хотелось бы изменить свойство YPos, чтобы стены всегда располагались друг над другом. Итак, если я изменю высоту четвертого этажа, все стены ниже него должны изменить свое значение YPos и ​​так далее. У меня это не получилось, и прошу вашей помощи!

Я попытался создать метод, который делает что-то вроде этого:

public void OrderWalls()
{
    for (int i = 0; i < _walls.Count; i++)
    {
        Walls[i].YPos = Walls[i-1].YPos + Walls[i].YHeight;
    }
}

Но я не могу понять, как применить этот метод в свойстве YPos. Или я все делаю неправильно? Любая помощь приветствуется!

Вот моя ViewModel и код моего главного окна:

public class MainWindowViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Wall> _walls;

    public ObservableCollection<Wall> Walls
    {
        get { return _walls; }
        set 
        { 
            _walls = value;
            OnPropertyChanged();
        }
    }

    public void OrderWalls()
    {
        for (int i = 0; i < _walls.Count; i++)
        {
            Walls[i].YPos = Walls[i-1].YPos + Walls[i].YHeight;
        }
    }

    public MainWindowViewModel() 
    {
        Walls = new ObservableCollection<Wall>();

        Walls.Add(new Wall { Floor = "Floor 4", Height = 3, Length = 6, YPos = 50, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 3", Height = 3, Length = 6, YPos = 200, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 2", Height = 3, Length = 6, YPos = 350, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 1", Height = 3, Length = 6, YPos = 500, XPos = 50 });
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
public partial class MainWindow : Window
{
    public MainWindow()
    {
        MainWindowViewModel viewModel = new MainWindowViewModel();
        this.DataContext = viewModel;
        InitializeComponent();
    }
}

Изменение, связанное с комментарием, сделанным в @BionicCode 11 июля:

Итак, я потратил довольно много времени, пытаясь реализовать ваш ответ. Вы используете множество функций, о которых я раньше не знал, поэтому мне пришлось провести небольшое исследование. Я думаю, что уже понимаю большую часть этого, но я определенно еще не достиг цели.

Я попробовал реализовать ваши предложения в своей программе, но без свойства "orientation", так как хочу пока просто держать его вертикальным (стараясь не перегружать себя), а также без свойства "IsArrangeItemEnabled", так как хочу, чтобы оно всегда было договариваться. Так что я не совсем скопировал ваш код. Надеюсь, я не нарушил при этом ни одну из запланированных функций.

При инициализации программы все YPos рассчитываются правильно. Однако когда я меняю значения в DataGrid, свойства YPos не обновляются, и я не могу понять, почему.

Я пытаюсь понять систему, с помощью которой все функции вызывают друг друга, и не могу понять, что должно обновлять свойства YPos. Я заметил, что в WallCollection.cs есть метод OnSizeChanged, который нигде не используется. Может ли это быть причиной?

Я думаю, мне бы очень помогло, если бы вы немного подробнее рассказали о функциях, используемых в WallCollection.cs, и о том, как они связаны друг с другом. Меня особенно смущают StartOberveSizeChanges и StopObserveSizeChanges, а также Dictionary/indexmap. Если у вас нет времени, я вас полностью пойму :)

WallCollection.cs

public class WallCollection : ObservableCollection<Wall>
{
    public WallCollection() { }
    private readonly Dictionary<Wall, int> indexMap = new();

    protected override void SetItem(int index, Wall item)
    {
        base.SetItem(index, item);
        AddItemInternal(index, item);
    }

    protected override void InsertItem(int index, Wall item)
    {
        base.InsertItem(index, item);
        AddItemInternal(index, item);
    }

    private void AddItemInternal(int index, Wall item)
    {
        if (!this.indexMap.ContainsKey(item))
        {
            StartObservingSizeChanges(item);
        }
        InitializeWallPosition(index, item);
    }

    private void InitializeWallPosition(int index, Wall item)
    {
        if (index > 0 && item is not null && double.IsInfinity(item.YPos))
        {
            Wall previousWall = this.Items[index - 1];
            item.YPos = previousWall.YPos + previousWall.YHeight;
        }
    }

    private void StartObservingSizeChanges(Wall item)
    {
        WeakEventManager<Wall, WallSizeChangedEventArgs>.AddHandler(item, nameof(Wall.SizeChanged), OnWallSizeChanged);
    }

    private void OnWallSizeChanged(object? sender, WallSizeChangedEventArgs e)
    {
        var changedWall = (Wall)sender;
        ArrangeWalls(changedWall, e.HasHeightChanged, e.HeightChange);
    }

    private void ArrangeWalls(Wall? changedWall, bool hasHeightChanged, double heightChange)
    {
        if (hasHeightChanged)
        {
            ArrangeItemsVertically(changedWall, heightChange);
        }
    }

    private void ArrangeItemsVertically(Wall changedWall, double change)
    {
        if (!this.indexMap.TryGetValue(changedWall, out int indexOfChangedWall))
        {
            return;
        }

        for (int index = indexOfChangedWall + 1; index <this.Items.Count; index++)
        {
            Wall wall = this.Items[index];
            wall.YPos += change;
        }
    }
}

WallSizeChangedEventArgs.cs

public class WallSizeChangedEventArgs : EventArgs
{
    public WallSizeChangedEventArgs(Size oldSize, Size newSize) 
    {
        this.OldSize = oldSize;
        this.NewSize = newSize;
    }

    public Size OldSize { get; }
    public Size NewSize { get; }
    public bool HasHeightChanged => this.OldSize.Height != this.NewSize.Height;
    public double HeightChange => this.NewSize.Height - this.OldSize.Height;
}

Стена.cs

public class Wall : INotifyPropertyChanged
{

    public Wall() 
    { 
        this.YPos = double.PositiveInfinity;
    }

    private const int _scale = 50;
    private double _height; //Height of wall IRL
    private double _length; //Lenght of wall IRL
    private double _xPos; //Horizonal position on canvas (Canvas.Left)
    private double _yPos; //Vertical position on canvas (Canvas.Top)
    
    public string Floor { get; set; }

    public double Height
    {
        get { return _height; }
        set 
        { 
            _height = value;
            OnPropertyChanged(nameof(this.YHeight));
        }
    }

    public double Length
    {
        get { return _length; }
        set 
        { 
            _length = value;
            OnPropertyChanged(nameof(this.XLength));
        }
    }

    public double XPos
    {
        get { return _xPos; }
        set 
        { 
            _xPos = value;
            OnPropertyChanged();
        }
    }

    public double YPos
    {
        get { return _yPos; }
        set 
        { 
            _yPos = value;
            OnPropertyChanged();
        }
    }

    public double XLength => this.Length * _scale;

    public double YHeight => this.Height * _scale;
    
    public event PropertyChangedEventHandler? PropertyChanged; //Unchanged
    public event EventHandler<WallSizeChangedEventArgs>? SizeChanged; //New
    protected void OnPropertyChanged([CallerMemberName] string name = null) //Unchanged
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

    protected virtual void OnSizeChanged(Size oldSize, Size newSize)
       => this.SizeChanged?.Invoke(this, new WallSizeChangedEventArgs(oldSize, newSize));
}

MainWindowViewModel.cs

public class MainWindowViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Wall> Walls { get; }
    public MainWindowViewModel() 
    {
        Walls = new WallCollection()
        {
            new Wall { Floor = "Floor 4", Height = 3, Length = 6, YPos = 50, XPos = 50 },
            new Wall { Floor = "Floor 3", Height = 3, Length = 6, XPos = 50 },
            new Wall { Floor = "Floor 2", Height = 3, Length = 6, XPos = 50 },
            new Wall { Floor = "Floor 1", Height = 3, Length = 6, XPos = 50 }
        };
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

Вам нужно вычислить положение y, которое представляет собой сумму высот этажей ниже текущего этажа. Этаж 1 = 0, Этаж 2 = 0 + 3, Этаж 3 = 0 + 3 + 1, Этаж 4 = 0 + 3 + 1 + 2

jdweng 04.07.2024 14:42

Вы можете использовать контейнер, который выполняет математические вычисления для измерения/упорядочения своих дочерних элементов и имеет свои собственные границы. Кстати, все свойства должны вызывать событие.

Sinatr 04.07.2024 15:06

Вам нужно будет подписаться на событие изменения свойств каждого объекта стены и пересчитывать их все для каждого изменения высоты. В вашем коде нет ничего, что обновляло бы все остальные стены при изменении. CollectionChanged срабатывает только при добавлении, удалении и т. д. коллекции, а не ее членов.

Felix Castor 04.07.2024 21:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
146
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ваши объекты стены должны реализовать INotifyPropertyChanged, и вам необходимо подписаться на PropertyChangedEvent для каждого из них. Вот что-то, что укажет вам правильное направление.

public class MainWindowViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Wall> _walls;

    public ObservableCollection<Wall> Walls
    {
        get { return _walls; }
        set 
        { 
            _walls = value;
            OnPropertyChanged();
        }
    }

    public void OrderWalls()
    {
        for (int i = 0; i < _walls.Count; i++)
        {
            Walls[i].YPos = Walls[i-1].YPos + Walls[i].YHeight;
        }
    }

    public MainWindowViewModel() 
    {
        Walls = new ObservableCollection<Wall>();

        Walls.Add(new Wall { Floor = "Floor 4", Height = 3, Length = 6, YPos = 50, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 3", Height = 3, Length = 6, YPos = 200, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 2", Height = 3, Length = 6, YPos = 350, XPos = 50 });
        Walls.Add(new Wall { Floor = "Floor 1", Height = 3, Length = 6, YPos = 500, XPos = 50 });

        foreach(var wall in Walls)
        {
           wall.PropertyChanged += WallChanged; // subscribe to all changes.
        }
    }

    private void WallChanged(object sender, PropertyChangedEventArgs e)
    {
         // May want to be selective on what changes you are worried
         // about. Only run when the height changes for instance.
         // Otherwise you could call this recursively and end up with a stack overflow.
         if (e.PropertyName == nameOf(Wall.Height))
             OrderWalls(); // Something changed update the y's.
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

Спасибо за ответ

mmdn 08.07.2024 07:14
Ответ принят как подходящий

Ваш код ошибочен. Во-первых, ваши свойства YPos и XPos не вызывают событие PropertyChanged. Это не позволит визуальным элементам обновлять свое положение на Canvas.

Далее, ваши расчеты также неверны. Самое главное, что ваш индекс выйдет за пределы допустимого диапазона для первой итерации, поскольку вы начнете с индекса i = 0 и ссылаетесь на коллекцию с помощью i - 1.
Затем вы добавляете высоту текущего элемента к вертикальному положению предыдущего элемента. Но вы должны добавить предыдущую высоту YHeight к предыдущей позиции YPos, чтобы получить новую позицию текущего элемента:

public void OrderWalls()
{
  if (!Walls.Any())
  {
    return;
  }

  for (int i = 1; i < _walls.Count; i++)
  {
    // This line will throw an IndexOutOfRangeException 
    // because in the first iteration 'index = i -1' results in 'index == -1'
    //Walls[i].YPos = Walls[i-1].YPos + Walls[i].YHeight;

    // Naming the elements usually visualizes what you are doing to help 
    // identifying logical and typing errors
    Wall currentWall = Walls[i];
    Wall previousWall = Walls[i - 1];
    currentWall.YPos = previousWall.YPos + previousWall.YHeight;
  }
}

Однако этот алгоритм можно значительно улучшить с точки зрения производительности, которая становится тем более актуальной, чем больше элементов содержит коллекция. Поскольку изменение высоты известно, вы можете использовать его, чтобы избежать полной итерации коллекции. Тот факт, что изменения высоты не влияют на предыдущие элементы, позволяет нам пропустить их.

Затем мы можем просто сместить вертикальное положение за счет изменения высоты. Класс Wall должен уведомлять об изменении своего размера, чтобы владелец элементов Wall мог переставлять объекты. По этой причине класс Wall должен реализовать событие SizeChanged.

Вам следует переместить всю связанную логику в выделенный класс. Например, вы можете создать пользовательский WallCollection, который прослушивает событие Wall.SizedChanged для изменения порядка элементов. Вы можете инкапсулировать поведение в любой класс, но этот класс должен иметь ссылку на коллекцию и отслеживать коллекцию на предмет добавления/удаления изменений, чтобы зарегистрировать/отменить регистрацию обработчиков событий SizeChanged. Эту логику проще иметь непосредственно в специализированной коллекции, поэтому я выбираю WallCollection.

WallSizeChangedEventArgs.cs
Данные о событии, чтобы предоставить наблюдателю подробную информацию об изменениях.

public class WallSizeChangedEventArgs : EventArgs
{
  public WallSizeChangedEventArgs(Size oldSize, Size newSize)
  {
    this.OldSize = oldSize;
    this.NewSize = newSize;
  }

  public Size OldSize { get; }
  public Size NewSize { get; }
  public bool HasHeightChanged => this.OldSize.Height != this.NewSize.Height;
  public bool HasWidthChanged => this.OldSize.Width != this.NewSize.Width;
  public double HeightChange => this.NewSize.Height - this.OldSize.Height;
  public double WidthChange => this.NewSize.Width - this.OldSize.Width;
}

Стена.cs
Смотрите комментарии, чтобы узнать об исправлениях.

public class Wall : INotifyPropertyChanged
{
  public Wall()
  {
    // Set the default. If the YPos value is left with this default
    // then the WallCollection will automatically stack items
    // with a gap of zero between the Wall items. Updating YPos will keep the gap at zero.
    // If the default is overwritten e.g. with 100, and this value would create a gap of e.g. 30
    // between the current and the preceding Wall item, then WallCollection will now update the YPos
    // by simply adding the offset. This way the vertical margin will always remain the same (30).
    this.YPos = double.PositiveInfinity;
  }

  // Introduce an event to notify about changes and make information about change details,
  // like the actual change value, available.
  // This helps to implement the re-arrange algorithm more efficiently
  // as we can now avoid the complete iteration of the collection.
  public event EventHandler<WallSizeChangedEventArgs>? SizeChanged;

  public event PropertyChangedEventHandler? PropertyChanged;

  // Prefer const over static (or static readonly)
  private const int Scale = 50;

  private double _height; //Height of wall IRL
  private double _length; //Lenght of wall IRL
  private double _xPos; //Horizonal position on canvas (Canvas.Left)
  private double _yPos; //Vertical position on canvas (Canvas.Top)

  // If this property doesn't raise the PropertyChanged event make it an auto-property
  public string Floor { get; set; }

  public double Height
  {
    get => _height;
    set
    {
      // We will only report the scaled size
      double oldHeight = this.YHeight;

      _height = value;

      // Avoid duplicate formula definition. Either execute from property setter (recommended) or move to method
      //_yHeight = value * Scale;

      // Use 'nameof' instead of magic strings for robustness. 
      //OnPropertyChanged("YHeight");
      OnPropertyChanged(nameof(this.YHeight));

      // We will only report the scaled size
      OnSizeChanged(new Size(this.XLength, oldHeight), new Size(this.XLength, this.YHeight));
    }
  }

  public double Length
  {
    get { return _length; }
    set
    {
      // We will only report the scaled size
      double oldWidth = this.XLength;

      _length = value;

      // Avoid duplicate formula definition. Either execute from property setter (recommended) or move to method
      //_xLength = value * Scale;

      // Use 'nameof' instead of magic strings for robustness and IDE support to avoid typos or enable symbol renaming. 
      //OnPropertyChanged("XLength");
      OnPropertyChanged(nameof(this.XLength));

      // We will only report the scaled size
      OnSizeChanged(new Size(oldWidth, this.YHeight), new Size(this.XLength, this.YHeight));
    }
  }

  // This property must raise the PropertyChanged event
  // to trigger the repositioning on the Canvas via data binding!
  public double XPos
  {
    get => _xPos;
    set
    {
      _xPos = value;
      OnPropertyChanged();
    }
  }

  // This property must raise the PropertyChanged event
  // to trigger the repositioning on the Canvas via data binding!
  public double YPos
  {
    get => _yPos;
    set
    {
      _yPos = value;
      OnPropertyChanged();
    }
  }

  // The setter is redundant as it discards the set value. Instead implement property as computed property.
  //
  // Width of Border on canvas (IRL Length * Scale)
  public double XLength => this.Length * Scale;
  //{
  //  get { return _xLength; }
  //
  //  // This setter is redundant as it discards the set value. Instead implement property as computed property
  //  set { _xLength = Length * Scale; }
  //}

  // The setter is redundant as it discards the set value. Instead implement property as computed property.
  //
  // Height of Border on canvas (IRL Height * Scale)
  public double YHeight => this.Height * Scale;
  //{
  //  get { return _yHeight; }

  //  // This setter is redundant as it discards the set value. Instead implement property as computed property
  //  set { _yHeight = Height * Scale; }
  //}

  protected virtual void OnSizeChanged(Size oldSize, Size newSize)
    => this.SizeChanged?.Invoke(this, new WallSizeChangedEventArgs(oldSize, newSize));

  protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

WallCollection.cs
Эта коллекция также автоматически будет складывать каждый добавленный элемент, переопределяя начальное значение YPos, если оно есть double.PositiveInfinity.

public class WallCollection : ObservableCollection<Wall>
{
  public WallCollection()
  { }

  public WallCollection(IEnumerable<Wall> collection) : base(collection)
  { }

  public WallCollection(List<Wall> list) : base(list)
  { }

  protected override void SetItem(int index, Wall item)
  {
    CheckReentrancy();

    // In case the index exists, the item will be replaced.
    // Therefore, we have to unhook all event handlers 
    // from the replaced item.
    RemoveItemInternal(index);

    // Add/replace the item
    base.SetItem(index, item);

    // Store the index in a hash table for fast lookup
    AddItemInternal(index, item);
  }

  protected override void InsertItem(int index, Wall item)
  {
    // Add the item
    base.InsertItem(index, item);

    // Store the index in a hash table for fast lookup
    AddItemInternal(index, item);
  }

  protected override void RemoveItem(int index)
    => RemoveItemInternal(index);

  protected override void ClearItems()
  {
    // Call CheckReentrancy to prevent this collection from being modifed while CollectionChanged event is raised.
    // The OnCollectionChanged implementation usually calls BlockReentrancy().
    // This is important as you want to prevent CollectionChanged event handlers to modify the collection
    // *before* all listeners have been notified (about the exact same state). 
    // You don't want that a "item added" ColectionChanged event becomes obsolete
    // because event handler A has e.g. removed the new item before event handler B could handle it.
    // Event handler B would still get the original notification about an added item.
    // But because event handler A has modified the collection, the event now reports an invalid state to event handler B.
    // This introduces a high potential for bugs. To avoid this, the collection is locked during CollectionChanged events.
    // If OnCollectionChanged has called BlockReentrancy() then calling CheckReentrancy()
    // from a modifying method will throw an exception until the bock has been lifted (all event handlers have been executed).
    // CheckReentrancy is usually called by the base methods SetItem(), InserItem(), RemoveItem() and ClearItems().
    // Because we have to unregister SizeChanged event handlers for each individual item
    // we have to perform the cleanup before manually clearing the collection.
    // We therefore don't invoke the base implementation of ClearItems() which is why we have to call CheckReentrancy by ourselves.
    CheckReentrancy();

    for (int index = this.Count - 1; index >= 0; index--)
    {
      RemoveItemInternal(index);
    }

    this.indexMap.Clear();
    OnPropertyChanged(new PropertyChangedEventArgs(nameof(this.Count)));
    OnPropertyChanged(new PropertyChangedEventArgs(nameof(Binding.IndexerName)));

    // This base method of ObservableCollection will internally call BlockReentrancy()
    // to lock this collection for modifications during the event execution
    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  }

  private void AddItemInternal(int index, Wall item)
  {
    if (!this.indexMap.ContainsKey(item))
    {
      this.indexMap.Add(item, index);
      StartObserveSizeChanges(item);
    }

    // Auto allign the added Wall
    InitializeVerticalWallPosition(index, item);
  }

  private void RemoveItemInternal(int index)
  {
    Wall removedItem = this[index];
    base.RemoveItem(index);
    StopObserveSizeChanges(removedItem);
    _ = this.indexMap.Remove(removedItem);
  }

  private void InitializeVerticalWallPosition(int index, Wall item)
  {
    // Only arrange when:
    if (index > 0  // item is not the first
        && item is not null
        && double.IsInfinity(item.YPos)) // item has no explicit vertical position assigned (the default YPos value is double.PositiveInfinity)
    {
      Wall previousWall = this.Items[index - 1];
      item.YPos = previousWall.YPos + previousWall.YHeight;

      ArrangeWalls(item, true, item.YHeight);
    }
  }

  private void StartObserveSizeChanges(Wall item)
  {
    // In case the items live longer than this collection (and the collection is not cleared before disregarded),
    // we must use a WeakEventManager to avoid a leak.
    WeakEventManager<Wall, WallSizeChangedEventArgs>.AddHandler(item, nameof(Wall.SizeChanged), OnWallSizeChanged);
  }

  private void StopObserveSizeChanges(Wall item)
  {
    // In case the items live longer than this collection (and the collection is not cleared before disregarded),
    // we must use a WeakEventManager to avoid a leak.
    WeakEventManager<Wall, WallSizeChangedEventArgs>.RemoveHandler(item, nameof(Wall.SizeChanged), OnWallSizeChanged);
  }

  // When a wall has changed its YHeight we must rearrange all succeeding Wall items.
  // A changing height means that the YPos offset of all succedding items has to be adjusted.
  private void OnWallSizeChanged(object? sender, WallSizeChangedEventArgs e)
  {
    var changedWall = (Wall)sender;
    ArrangeWalls(changedWall, e.HasHeightChanged, e.HeightChange);
  }

  private void ArrangeWalls(Wall? changedWall, bool hasHeightChanged, double heightChange)
  {
    if (hasHeightChanged)
    {
      ArrangeItemsVertically(changedWall, heightChange);
    }
  }

  private void ArrangeItemsVertically(Wall changedWall, double change)
  {
    // If  there wasn't the Dictionary we would have to call IndexOf(changedWall) to get the current index.
    // And then increment the index to get the succeeding Wall items.
    // This is a O(n) operation while querying the Dictionary is O(1). This means the perfomance is not depedeing on the size of collection.
    // We will always get the index immediately.
    if (!this.indexMap.TryGetValue(changedWall, out int indexOfChangedWall))
    {
      return;
    }

    // Start from the changed item to improve the perfromance.
    // The preceding items are not affected by the size change of the current item.
    for (int index = indexOfChangedWall + 1; index < this.Items.Count; index++)
    {
      Wall wall = this.Items[index];

      // For example, if the heigth of Wall A has changed by +10 or -20
      // then me must vertically offset all succeeding Wall items by +10 or -20
      wall.YPos += change;
    }
  }

  // The map that stores the index of each Wall for fast lookup.
  // This is to avoid IndexOf calls as this will perform bad for big collections.
  private readonly Dictionary<Wall, int> indexMap = new();
}

MainWindowViewModel.cs

public class MainWindowViewModel : INotifyPropertyChanged
{
  // A binding source collection should always be 
  // a readonly property that returns an ObservableCollection.
  public WallCollection Walls { get; }

  public MainWindowViewModel()
  {
    // Only set the YPos of the first item. 
    // The WallCollection will stack them properly. 
    // If you want to start at YPos == 0 then you don't even have 
    // to set the YPos of the first item. 
    // Any explicit YPos value set will have precedence over the 
    // calculated arrangement of the WallCollection.
    Walls = new WallCollection()
    {
      new Wall { Floor = "Floor 4", Height = 3, Length = 6, YPos = 50, XPos = 50 },
      new Wall { Floor = "Floor 3", Height = 3, Length = 6, XPos = 50 },
      new Wall { Floor = "Floor 2", Height = 3, Length = 6, XPos = 50 },
      new Wall { Floor = "Floor 1", Height = 3, Length = 6, XPos = 50 }
    };
  }

  public event PropertyChangedEventHandler? PropertyChanged;
  protected void OnPropertyChanged([CallerMemberName] string name = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
  }
}

Большое спасибо за этот полезный и подробный ответ. Это очень ценится. Я узнал больше, чем ожидал :-)

mmdn 08.07.2024 07:14

Большое спасибо за ваш отзыв и высокую оценку. Я рад, что вы нашли этот ответ полезным. Спасибо!

BionicCode 08.07.2024 13:41

Привет. Еще раз спасибо за ваше время. Я попробовал реализовать ваши предложения, но у меня также есть несколько дополнительных вопросов. Надеюсь, вы найдете время взглянуть. Посмотрите редактирование моего сообщения

mmdn 11.07.2024 14:07

Эй, нет проблем. Извините за дополнительную сложность ответа. Дополнительную функциональность было действительно легко реализовать без дополнительных усилий. Я думал, это сделает функциональность коллекции более округлой. Но я понимаю, что это отвлекает от самого главного, особенно если пытаешься понять основы реализации.

BionicCode 11.07.2024 14:27

Я должен извиниться и надеюсь, что не причинил вам слишком много головной боли. Я переработаю реализацию и добавлю несколько аннотаций, которые могут помочь вам лучше понять детали реализации. Исходя из этого, вы можете задавать более целенаправленные вопросы, чтобы получить более конкретные ответы.

BionicCode 11.07.2024 14:28

Хорошо, я обновил код: удалил все, о чем вы не просили, а также добавил и расширил комментарии, чтобы объяснить, почему и что. Рекомендую прочитать код сверху. Ошибка, о которой вы сообщили, действительно была связана с тем, что OnSizeChanged никогда не вызывался. Теперь он вызывается из свойств Height и Length. Но сообщаемый размер будет основан на масштабированных размерах YHeight и XLength. Если вы хотите сообщить об обоих размерах (масштабированном и исходном), вы можете расширить SizeChangedEventArgs, добавив связанные свойства (коэффициента масштабирования может быть достаточно).

BionicCode 11.07.2024 15:33

Спасибо, что так быстро ответили мне! Спасибо за добавление дополнительных комментариев. Теперь я лучше понимаю некоторые предполагаемые функции :) Однако YPos, похоже, не обновляется. По крайней мере, стены не складываются. Я почти скопировал ваш код. Я заметил в Wall.cs в установщике высоты и длины: при вызове OnSizeChanged(oldSize,newSize) мы не указываем один и тот же размер дважды, поскольку мы говорим oldHeight = this.YHeight. В момент выполнения OnSizeChanged будет ли this.Yheight старой или новой высотой?

mmdn 12.07.2024 11:19

И еще немного побочный вопрос. Есть ли способ следить за XPos, YPos и ​​т. д. для каждой стены во время работы программы, и я меняю значения в DataGrid и т. д. Я пробовал реализовать какой-то foreach(стена стены в стенах){Console.WriteLine(Walls[i].YPos)}, но я могу" Кажется, что он не достигает коллекции Walls откуда угодно (надеюсь, это имеет смысл). Нет ли какого-нибудь (простого) способа постоянно отслеживать свойства каждого элемента, как я мог бы это сделать в консольном приложении? Продолжение..

mmdn 12.07.2024 11:19

Я знаю, что могу поставить точку останова, чтобы видеть элементы в «Локальных», но редко могу понять, где поставить точку останова, особенно при возрастающей сложности.

mmdn 12.07.2024 11:21

Чтобы исправить расположение, я забыл добавить элементы в таблицу. Проверьте AddItemInternal на наличие изменений. Мне жаль, что я причинил вам эти неприятности. Я написал код здесь, на Stackoverflow. Я должен был протестировать хотя бы коллекцию. Я думал, что это так просто, но недостаток внимания заставил меня совершить эту ошибку. Мне жаль.

BionicCode 12.07.2024 14:19

Что касается вашего вопроса: вы можете добавить Debug.WriteLine в установщик свойств. Код, связанный с Debug, будет скомпилирован только в режиме отладки. В режиме выпуска все ссылки Debug игнорируются компилятором. Альтернативно вы можете использовать регистратор. Преимущество в том, что вы более гибки. Вы можете настроить регистратор для входа в файл, базу данных, электронную почту, консоль или любой другой приемник, который вы предоставите. Кроме того, форматирование вывода становится более гибким. Есть несколько хороших регистраторов, таких как Serilog или Log4net. Просто проведите небольшое исследование.

BionicCode 12.07.2024 14:22

Извините, наверное, я немного неконкретно высказался по поводу фиксированного кода. Это метод AddItemInternal в классе WallCollection. Просто добавил одну строчку.

BionicCode 12.07.2024 14:26

Теперь это действительно работает! Большое спасибо за ваше время и терпение

mmdn 12.07.2024 17:34

Пожалуйста. Я рад, что мы это исправили.

BionicCode 12.07.2024 19:11

Извините, вынужден связаться с вами. Я обновил метод WallCollection.SetItem. Его не хватало для обработки случая замены. В этом случае мы должны отсоединить все обработчики событий от заменяемого элемента. Вам нужно только скопировать/обновить метод SetItem. Я забыл это исправить. Я все время думал об этом, но потом как-то забыл об этом. Но воспоминания вернулись всего несколько минут назад. Исправление актуально только при замене элементов: wallCollectio[index] = item. Сорри об этом.

BionicCode 13.07.2024 09:53

Другие вопросы по теме