WPF でビューモデルからスクロールを行う

WPF で MVVM なプログラムを組んでいる時に、「DataGrid の表示を末尾までスクロールさせたい」とか「1行下にスクロールさせたい」というときがありますよね。それを添付プロパティを用いることでビューモデルから行えるようにしてみました。対象を Control にしているので、TextBox のスクロールも行うことができます 😉

スクロールは、添付プロパティにバインドさせたビューモデルのプロパティに、列挙型 Scroll の列挙子「LineDown」「LineUp」「ToBottom」「ToTop」をセットすることで行うようになっています(スクロール後ビューモデルのプロパティは「None」にリセットされます)。

プログラムは使用例もいっしょに書いています。
画面はこんな感じで。

サンプルの画面
サンプルの画面

各ボタンをクリックすると DataGrid の表示行がスクロールします。

それではプログラムです。
新しいプロジェクトで WPF を選択してプロジェクトを作成します(例ではプロジェクト名を「ControlScroll」にしています)。

まずは列挙型から。
プロジェクトに「Behaviors」フォルダを作成し、Behaviors フォルダの下に「Interfaces」フォルダを作成します。Interfaces フォルダに「Scroll」列挙型を作ります(追加でクラスを選択して書き換えるなり、コードファイルを選択するなりしてください)。

namespace ControlScroll.Behaviors.Interfaces
{
    public enum Scroll
    {
        None,
        LineDown,
        LineUp,
        ToBottom,
        ToTop
    }
}

次に Behaviors フォルダに「ControlScroll」クラスを作ります。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

using ControlScroll.Behaviors.Interfaces;

namespace ControlScroll.Behaviors
{
    public static class ControlScroll
    {
        public static readonly DependencyProperty KickScrollProperty = DependencyProperty.RegisterAttached(
            "KickScroll", typeof(Scroll), typeof(ControlScroll),
            new FrameworkPropertyMetadata
            {
                DefaultValue = Scroll.None,
                PropertyChangedCallback = KickScrollChanged,
                BindsTwoWayByDefault = true
            });
        public static Scroll GetKickScroll(DependencyObject obj)
        {
            return (Scroll)obj.GetValue(KickScrollProperty);
        }
        public static void SetKickScroll(DependencyObject obj, Scroll value)
        {
            obj.SetValue(KickScrollProperty, value);
        }

        private static void KickScrollChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as Control;
            if (control == null) return;
            if ((Scroll)e.NewValue == Scroll.None) return;

            if (VisualTreeHelper.GetChildrenCount(control) == 0) return; // 子要素が取れる状態かを確認
            var decorator = VisualTreeHelper.GetChild(control, 0) as Decorator;
            if (decorator == null) return;

            var scrollViewer = decorator.Child as ScrollViewer;
            if (scrollViewer != null)
            {
                switch ((Scroll)e.NewValue)
                {
                    case Scroll.LineDown:
                        scrollViewer.LineDown();
                        break;
                    case Scroll.LineUp:
                        scrollViewer.LineUp();
                        break;
                    case Scroll.ToBottom:
                        scrollViewer.ScrollToBottom();
                        break;
                    case Scroll.ToTop:
                        scrollViewer.ScrollToTop();
                        break;
                }
            }
            SetKickScroll(control, Scroll.None); // プロパティ値を None に戻す
        }
    }
}

KickScrollProperty に設定している FrameworkPropertyMetadata の PropertyChangedCallback に KickScrollChanged メソッドを設定することで KickScrollProperty 変更時に KickScrollChanged メソッドが呼び出されるようにしています。その KickScrollChanged メソッドではプロパティ変更後の値にしたがってスクロールを行うようにしています。スクロールの対象は、イベント ソースの子ビジュアル オブジェクトから取得する ScrollViewer のオブジェクトになります。

あとは使用例のモデル、ビューとビューモデルです。
プロジェクトに「Models」フォルダを作成し、Models フォルダに「Sample」クラスを作ります。

namespace ControlScroll.Models
{
    public class Sample
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
}

次にビューモデルですが、MVVM パターンなので、お決まりのコマンド処理とプロパティチェンジイベントの発火を提供する ViewModelBase クラスは別記事の ViewModelBase で書いたものを使っています。別プロジェクトにして作成した DLL を参照設定で追加するなり、プロジェクト内にソースを取り入れるなりしてください(次のコードは DLL を参照する前提で書いています)。

プロジェクトに「ViewModels」フォルダを作成し、ViewModels フォルダに「MainWindowViewModel」クラスを作ります。

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;

using MakCraft.ViewModels;

using ControlScroll.Behaviors.Interfaces;
using ControlScroll.Models;

namespace ControlScroll.ViewModels
{
    class MainWindowViewModel : ViewModelBase
    {
        private ObservableCollection<Sample> _oc_samples;

        public MainWindowViewModel()
        {
            initData();
        }

        private CollectionViewSource _cvs_sample;
        public ICollectionView Samples
        {
            get { return _cvs_sample.View; }
        }

        private Scroll _kickScroll;
        public Scroll KickScroll
        {
            get { return _kickScroll; }
            set
            {
                _kickScroll = value;
                base.RaisePropertyChanged(() => KickScroll);
            }
        }

        private void ScrollDownExecute()
        {
            KickScroll = Scroll.LineDown;
        }
        private ICommand _scrollDownCommand;
        public ICommand ScrollDownCommand
        {
            get
            {
                if (_scrollDownCommand == null)
                    _scrollDownCommand = new RelayCommand(ScrollDownExecute);
                return _scrollDownCommand;
            }
        }

        private void ScrollUpExecute()
        {
            KickScroll = Scroll.LineUp;
        }
        private ICommand _scrollUpCommand;
        public ICommand ScrollUpCommand
        {
            get
            {
                if (_scrollUpCommand == null)
                    _scrollUpCommand = new RelayCommand(ScrollUpExecute);
                return _scrollUpCommand;
            }
        }

        private void ScrollBottomExecute()
        {
            KickScroll = Scroll.ToBottom;
        }
        private ICommand _scrollBottomCommand;
        public ICommand ScrollBottomCommand
        {
            get
            {
                if (_scrollBottomCommand == null)
                    _scrollBottomCommand = new RelayCommand(ScrollBottomExecute);
                return _scrollBottomCommand;
            }
        }

        private void ScrollTopExecute()
        {
            KickScroll = Scroll.ToTop;
        }
        private ICommand _scrollTopCommand;
        public ICommand ScrollTopCommand
        {
            get
            {
                if (_scrollTopCommand == null)
                    _scrollTopCommand = new RelayCommand(ScrollTopExecute);
                return _scrollTopCommand;
            }
        }

        private void initData()
        {
            //_oc_samples = new ObservableCollection<Sample>();
            //*
            _oc_samples = new ObservableCollection<Sample> {
                new Sample { Id = 1, Name = "サンプル01", Email = "sample01@example.com" },
                new Sample { Id = 2, Name = "サンプル02", Email = "sample02@example.com" },
                new Sample { Id = 3, Name = "サンプル03", Email = "sample03@example.com" },
                new Sample { Id = 4, Name = "サンプル04", Email = "sample04@example.com" },
                new Sample { Id = 5, Name = "サンプル05", Email = "sample05@example.com" },
                new Sample { Id = 6, Name = "サンプル06", Email = "sample06@example.com" },
                new Sample { Id = 7, Name = "サンプル07", Email = "sample07@example.com" },
                new Sample { Id = 8, Name = "サンプル08", Email = "sample08@example.com" },
                new Sample { Id = 9, Name = "サンプル09", Email = "sample09@example.com" },
                new Sample { Id = 10, Name = "サンプル10", Email = "sample10@example.com" },
                new Sample { Id = 11, Name = "サンプル11", Email = "sample11@example.com" },
                new Sample { Id = 12, Name = "サンプル12", Email = "sample12@example.com" },
                new Sample { Id = 13, Name = "サンプル13", Email = "sample13@example.com" },
                new Sample { Id = 14, Name = "サンプル14", Email = "sample14@example.com" },
                new Sample { Id = 15, Name = "サンプル15", Email = "sample15@example.com" },
                new Sample { Id = 16, Name = "サンプル16", Email = "sample16@example.com" },
                new Sample { Id = 17, Name = "サンプル17", Email = "sample17@example.com" },
                new Sample { Id = 18, Name = "サンプル18", Email = "sample18@example.com" },
                new Sample { Id = 19, Name = "サンプル19", Email = "sample19@example.com" },
            };
             //*/

            _cvs_sample = new CollectionViewSource { Source = _oc_samples };
        }
    }
}

最後に MainWindow.xaml です。

<Window x:Class="ControlScroll.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:b="clr-namespace:ControlScroll.Behaviors"
        xmlns:vm="clr-namespace:ControlScroll.ViewModels"
        Title="MainWindow" Width="525" SizeToContent="Height">
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    
    <StackPanel>
        <TextBlock Text="DataGrid のスクロール操作のサンプル" FontSize="18" HorizontalAlignment="Center" Margin="8" />
        
        <DataGrid
            ItemsSource="{Binding Samples}" IsReadOnly="True" MaxHeight="200" Margin="4"
            b:ControlScroll.KickScroll="{Binding KickScroll}" />
        
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="4 4 4 0">
            <Button
                Content="一行上にスクロール" MinWidth="200" HorizontalAlignment="Right" Margin="4 0 0 0"
                Command="{Binding ScrollUpCommand}" />
            <Button
                Content="一行下にスクロール" MinWidth="200" HorizontalAlignment="Right" Margin="4 0 0 0"
                Command="{Binding ScrollDownCommand}" />
        </StackPanel>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="4 4 4 0">
            <Button
                Content="先頭までスクロール" MinWidth="200" HorizontalAlignment="Right" Margin="4 10 0 10"
                Command="{Binding ScrollTopCommand}" />
            <Button
                Content="末尾までスクロール" MinWidth="200" HorizontalAlignment="Right" Margin="4 10 0 10"
                Command="{Binding ScrollBottomCommand}" />
        </StackPanel>

    </StackPanel>
</Window>

DataGrid 要素に属性として ControlScroll 添付プロパティを指定し、KickScroll プロパティにビューモデルの KickScroll プロパティをバインドしています。

以上でプログラムが動きます 🙂


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です