none
WPF MVVM Datagrid scrolling RRS feed

  • Вопрос

  • Нашел на этом форуме два варианта:

    Можно скролить dataGrid так:
    
    1) использовать метод ScrollIntoView:
    // при изменении коллекции скролим к последнему элементу      
    mystr.Add(new test() { Text = "test" });
    dataGrid1.ScrollIntoView(mystr[mystr.Count-1]); 
    
    2) скролить ScrollViewer у DataGrid-а:
          // изменение коллекции
          mystr.Add(new test() { Text = "test2" });
          if (dataGrid1.Items.Count > 0)
          {
            var border = VisualTreeHelper.GetChild(dataGrid1, 0) as Decorator;
            if (border != null)
            {
              var scroll = border.Child as ScrollViewer;
              if (scroll != null) scroll.ScrollToEnd();
            }
          }

    Однако, оба этих варианта требуют не только code-behind и присвоения имени для датагрида, но наличия ссылки на вью, что не очень сочетается с MVVM, где биндинг идет через DataContext. Есть ли другие варианты решения этой проблемы, более укладывающиесся в шаблон MVVM?

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

    Требуется решение, позволяющее обеспечивать скроллинг внутри вью, пусть даже в code-behind, необязательно только в размете.




    • Изменено 43534543 17 июля 2014 г. 8:49
    17 июля 2014 г. 7:34

Ответы

  • Решил проблему так:

    В разметки внтурь  грида вставил:

                                    <i:Interaction.Behaviors>
                                        <v:ScrollToEndBehavior/>
                                    </i:Interaction.Behaviors>

    В код-бехайнде окна сделал:

        public class ScrollToEndBehavior : Behavior<ItemsControl>
        {
            /// <summary>
            /// Обеспечивает передачу ссылки на конкретный ItemsControl для скроллинга на последний элемент.
            /// </summary>
            private ItemsControl _itemsControl;
    
            protected override void OnAttached()
            {
                base.OnAttached();
                
                var sourceCollection = this.AssociatedObject.ItemsSource as ObservableSortedDictionary<long, тип элемента коллекции>;
                if (sourceCollection == null) return;
    
                sourceCollection.CollectionChanged +=
                    new NotifyCollectionChangedEventHandler(DataGridCollectionChanged);
    
                _itemsControl = this.AssociatedObject;
            }
    
            /// <summary>
            /// Обеспечивает скролл на последний добавленный элемент.
            /// </summary>
            private void DataGridCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (_itemsControl != null)
                {
                    var border = VisualTreeHelper.GetChild(_itemsControl, 0) as Decorator;
                    if (border != null)
                    {
                        var scroll = border.Child as ScrollViewer;
                        if (scroll != null) scroll.ScrollToEnd();
                    }
                }
            }
        }

    6 августа 2014 г. 13:18

Все ответы

  • Есть такая штука, как Attached Properties и, из аттачед свойств как следствие - Behaviors.

    Behaviors tutorial

    Попробуйте сделать там Dependency свойство с типом Вашего айтема, и при срабатывании PropertyChangedCallback"а скролльте Ваш датагрид.

    17 июля 2014 г. 9:50
  • Пока сделал в code-behind View так:

            /// <summary>
            /// Обеспечивает перекидку ссылки на этот грид для скроллинга на последний элемент.
            /// </summary>
            private DataGrid _dgUI;
    
            private void DgUILoaded(object sender, RoutedEventArgs e)
            {
                var dg = (DataGrid)sender;
                if (dg == null || dg.ItemsSource == null) return;
                _dgUI = dg; // запомнил ссылку на грид
    
                var sourceCollection = dg.ItemsSource as ObservableSortedDictionary<long, Data>; ;
                if (sourceCollection == null) return;
    
                sourceCollection.CollectionChanged +=
                    new NotifyCollectionChangedEventHandler(DataGridCollectionChanged); //подписался на изменение коллекции
            }
    
            private void DataGridCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                WPFSupportMethods.ScrollToEnd(_dgUI); //сам скроллинг
            }

     

    Правда работает только с одним гридом. Вот содержимое View:

        <ListView>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <DockPanel 
    
                        <DataGrid Name="dgUI"/>
    
                    </DockPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    Скролить будет только последний  DataGrid  из коллекции гридов.


    • Изменено 43534543 18 июля 2014 г. 8:37
    18 июля 2014 г. 8:24
  • Есть такая штука, как Attached Properties и, из аттачед свойств как следствие - Behaviors.

    Попробуйте сделать там Dependency свойство с типом Вашего айтема, и при срабатывании PropertyChangedCallback"а скролльте Ваш датагрид.

    Сделать Dependency свойство где, во вьюмодели?

    В code-behind View  я сделал класс ScrollToEndBehavior : Behavior<DataGrid>, доступ к гриду у меня получается есть, но к какому событию мне подписываться-то, чтобы скроллить? То есть, я пишу в свойстве поведения:

    protected override void OnAttached() { base.OnAttached(); this.AssociatedObject.а тут что? += скролящий метод

    }









    • Изменено 43534543 31 июля 2014 г. 13:08
    31 июля 2014 г. 12:40
  • Создайте dependendency property в классе behavior с типом Вашей коллекции. Т.к. у Вас ObservableCollection, то можете подписываться в behavior на CollectionChanged и скролльте как нужно.  
    31 июля 2014 г. 13:11
  • Создайте dependendency property в классе behavior с типом Вашей коллекции. Т.к. у Вас ObservableCollection, то можете подписываться в behavior на CollectionChanged и скролльте как нужно.  

    1. А для чего нужно зависимое свойство с типом моей коллекции?

    2. Чтобы подписаться к CollectionChanged из code-behind View, надо иметь ссылку на вьюмодель, которая эту коллекцию содержит. Как мне обратиться к этой коллекции? При создании View никаких ссылок на ViewModel не передается, биндинг идет через DataContext.

        public class ScrollToEndBehavior : Behavior<DataGrid>
        {
            public static readonly DependencyProperty MyCustomProperty =
            DependencyProperty.Register("MyCustom", typeof(Record), typeof(????));
    
            public Record MyCustom
            {
                get { return GetValue(MyCustomProperty) as Record; }
                set { SetValue(MyCustomProperty, value); }
    
            }
    
            protected override void OnAttached()
                {
                    base.OnAttached();
    
                    //присоединение обработчиков событий
                    ???????.CollectionChanged += new NotifyCollectionChangedEventHandler (ScrollToEnd);
             
                    //this.AssociatedObject.  += ScrollToEnd;
    
                }
        }


    • Изменено 43534543 31 июля 2014 г. 14:04
    31 июля 2014 г. 13:40
  • Так прибиндите коллекцию к ScrollToEndBehavior.YourCollection.

    У него последним аргументом есть PropertyChangedCallBack, там Вы отследите когда изменяется коллекция. Это именно когда будет обновление PropertyChanged  во вью моделе. А this.AssociatedObject это и есть Ваш DataGrid. Итого таким способом Вы и коллекцию имеете, которую прибиндили и с контролом можете поработать не в код бехайне страницы.

    UPD: Или же не создавайте DependencyProperty, а подпишитесь у AssociatedObject на Loaded, как Вы это делали в код бехайне и делайте что Вам нужно. Это в любом случае избавит Вас от использования код-бехайна страницы.
    • Изменено Oleg Kurzov 31 июля 2014 г. 13:47
    31 июля 2014 г. 13:45
  • Так прибиндите коллекцию к ScrollToEndBehavior.YourCollection.

    Если все равно писать всякие костыли в коде, обеспечивающие требуемую работу разметки, то почему-бы не в code-behind страницы - так что я забил на это и пишу все в  code-behind, где грид и т.д.. 

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

    1. Где и как я буду биндить RecordsProperty на коллекцию из ViewModel-и?

    2. Зачем вообще тогда  Behavior, если я и так забиндюсь на коллекцию и смогу получать события ее изменения? Чтобы уйти от  code-behind? 

        public class ScrollToEndBehavior : Behavior<DataGrid>
        {
            public static readonly DependencyProperty RecordsProperty =
            DependencyProperty.Register("Records", typeof(Record), typeof(FrameworkElement), new FrameworkPropertyMetadata(false, AttachedChanged));
    
            public Record Records
            {
                get { return GetValue(RecordsProperty) as Record; }
                set { SetValue(RecordsProperty, value); }
            }
    
            private static void AttachedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
            {
                //отсюда вызываю скроллинг?
            }
    
            protected override void OnAttached()
            {
                base.OnAttached();
                //присоединение обработчиков событий
    
            
                //this.AssociatedObject.Loaded +=
    
            }
    
            private void ScrollToEnd(DataGrid dataGrid)
            {
    
            }
        }

     Я извиняюсь, но я реально не могу понять, хоть и почитал уже про свойства зависимостей. 

    Мне уже пофиг на код-бехайнд, мне надо, чтобы просто скроллинг работал, причем с каждым из гридов (у меня коллекция гридов).

    • Изменено 43534543 31 июля 2014 г. 14:53
    31 июля 2014 г. 14:48
  • А, у Вас еже датагрид в листвью, а то я уже сам немного запутался :)

    В общем, Вам нужно, что бы все датагрииды в листвью скролиллись в конец?

    31 июля 2014 г. 15:01
  • Да, но в той схеме, которую я описал как временное решение, у меня скроллится грид только у вью, которое было создано последним (то есть вьюмодель которого была последней в списке вьюмоделей, к которому прибинден ListView). Что кстати для меня непонятно, ведь для каждого элемента (там коллекция вьюмоделей) создается свое View, со своей переменной, в которой запомнен грид, и подписка происходит изнутри каждого экземпляра вью.

    И проблема основная для меня в том, что у меня во Вью нет ссылки на Вьюмодель, там сопоставление происходит через селектор (эту схему я конечно не сам написал, ее мне подсказали на этом форуме).

    Сопоставление происходит в разметке МейнВиндоу:

            <v:ViewContentTemplateSelector x:Key="viewContentTemplateSelector"
                                           ViewListTemplate="{StaticResource viewListTemplate}"
                                           ViewTabTemplate="{StaticResource viewTabTemplate}" />

    Короче говоря, у меня вообще нет кода типа View view = new View(), поэтому я не могу приделать туда передачу ссылки на Вьюмодель.







    • Изменено 43534543 31 июля 2014 г. 15:34
    31 июля 2014 г. 15:08
  • А, у Вас еже датагрид в листвью, а то я уже сам немного запутался :)

    В общем, Вам нужно, что бы все датагрииды в листвью скролиллись в конец?

     Проблема актуальна, как сделать через  dependendency property я не разобрался: где и как я могу забиндить RecordsProperty на коллекцию из ViewModel-и?
    5 августа 2014 г. 11:35
  • Еще раз сформулирую  проблему: есть коллекция View, в каждом из которых есть грид. Требуется, чтобы каждый грид скролился в конец при появлении новой  записи в коллекции, находящейся в сопоставленной ему ViewModel (то есть для каждой ViewModel есть свой View). Средствами xaml как я понял этого не сделать, только через код C#, что приводит к необходимости решения двух проблем:

    1. Как получить событие изменения грида или коллекции.

    2. Как получить доступ к гриду именного того вью, который нужно осткролить.

    Code-behind Вью: 

      /// <summary>
            /// Обеспечивает перекидку ссылки на этот грид для скроллинга на последний элемент.
            /// </summary>
            private DataGrid _dgUI;
    
            private void DgUILoaded(object sender, RoutedEventArgs e)
            {
                var dg = (DataGrid)sender;
                if (dg == null || dg.ItemsSource == null) return;
                _dgUI = dg; // запомнил ссылку на грид
    
                var sourceCollection = dgUI.ItemsSource as ObservableSortedDictionary<long, Data>; ;
                if (sourceCollection == null) return;
    
                sourceCollection.CollectionChanged +=
                    new NotifyCollectionChangedEventHandler(DataGridCollectionChanged); //подписался на изменение коллекции
            }
    
            private void DataGridCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                WPFSupportMethods.ScrollToEnd(_dgUI); //сам скроллинг
            }

    То есть в разметке производится подписка на событие Loaded грида, с целью получить ссылку на него, ссылка запоминается, а затем (так как каждый грид через разметку биндится через ItemsSource), через ссылку на грид получается ссылка на коллекцию Вьюмодели, на которую он соответственно забинден, к которой и производится подписка на событие изменения коллекции.

    В приведенном коде, подписка на все коллекции sourceCollection происходит корректно, то есть событие  DataGridCollectionChanged поднимается при изменении любой из sourceCollection, однако в  _dgUI всегда находится только самый последний грид, соответствено скролит только его, независимо от того, какая sourceCollection  подняла событие. Почему так происходит, почему переменная  _dgUI из сode-behind Вью работает, как статическая, то есть единственная для всех экземпляров View?

    • Изменено 43534543 5 августа 2014 г. 15:08
    5 августа 2014 г. 14:52
  • Решил проблему так:

    В разметки внтурь  грида вставил:

                                    <i:Interaction.Behaviors>
                                        <v:ScrollToEndBehavior/>
                                    </i:Interaction.Behaviors>

    В код-бехайнде окна сделал:

        public class ScrollToEndBehavior : Behavior<ItemsControl>
        {
            /// <summary>
            /// Обеспечивает передачу ссылки на конкретный ItemsControl для скроллинга на последний элемент.
            /// </summary>
            private ItemsControl _itemsControl;
    
            protected override void OnAttached()
            {
                base.OnAttached();
                
                var sourceCollection = this.AssociatedObject.ItemsSource as ObservableSortedDictionary<long, тип элемента коллекции>;
                if (sourceCollection == null) return;
    
                sourceCollection.CollectionChanged +=
                    new NotifyCollectionChangedEventHandler(DataGridCollectionChanged);
    
                _itemsControl = this.AssociatedObject;
            }
    
            /// <summary>
            /// Обеспечивает скролл на последний добавленный элемент.
            /// </summary>
            private void DataGridCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                if (_itemsControl != null)
                {
                    var border = VisualTreeHelper.GetChild(_itemsControl, 0) as Decorator;
                    if (border != null)
                    {
                        var scroll = border.Child as ScrollViewer;
                        if (scroll != null) scroll.ScrollToEnd();
                    }
                }
            }
        }

    6 августа 2014 г. 13:18