본문 바로가기
C#/WPF

[WPF] MVVM Scroll 동작 구현

by HJ0216 2024. 3. 31.

최근에 프로젝트를 진행하면서 ListView Control을 하며 어려웠던 부분을 정리하였습니다.

 

👉 기본 환경

- Language: C#, xaml

- IDE: Visual Basic 2022

- Framework: .NET 8.0


Scroll 처리를 버튼으로 만든 것..

제가 구현했던 동작 중 난이도 최상..!

 

✍️ ListView에 이미지를 클릭할 때, 스크롤이 이동하면서 선택된 이미지가 중앙에 오는 동작입니다.

 

 

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<Window>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
 
        <!--// Window //-->
        <Grid Grid.Row="0">
            
        </Grid>
        
        <!--// Image //-->
        <Grid Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="100"/>
            </Grid.RowDefinitions>
            
            <!--// Selected Image //-->
            <Image Grid.Row="0" Source="{Binding SelectedImageSource}"/>
            
            <!--// Image List //-->
            <ListView Name="listViewImageSources" 
                      Grid.Row="1"
                      ItemsSource="{Binding ImageSources}"
                      SelectedValue="{Binding SelectedImageSource, Mode=TwoWay}"
                      SelectedIndex="{Binding SelectedImageSourceIndex, Mode=TwoWay}"
                      ScrollViewer.VerticalScrollBarVisibility="Disabled"
                      ScrollViewer.HorizontalScrollBarVisibility="Hidden"
                      >
                <b:Interaction.Triggers>
                    <b:EventTrigger EventName="MouseLeftButtonUp">
                        <b:InvokeCommandAction Command="{Binding ScrollImageCommand}"
                                               CommandParameter="{Binding ElementName=listViewImageSources}"
                                               />
                    </b:EventTrigger>
                </b:Interaction.Triggers>
                <ListView.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center"
                                    />
                    </ItemsPanelTemplate>
                </ListView.ItemsPanel>
 
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem">
                        <Setter Property="Width" Value="75"/>
                        <Setter Property="Height" Value="75"/>
                    </Style>
                </ListView.ItemContainerStyle>
                
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <Image Source="{Binding .}" Stretch="Fill"/>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>
        
        <!--// Etc.. //-->
        <Grid Grid.Row="2">
            
        </Grid>
    </Grid>
</Window>
 

 

ListView

  * SelectedValue와 SelectedIndex를 모두 설정한 이유는 

    * SelectedValue: Image Source를 바인딩하기 위함이고,

    * SelectedIndex: Scroll 위치 계산에 사용하기 위함입니다.

  * ScrollViewer는 Hidden 처리하고 동작을 Select 이벤트에 넘겼습니다.

  * InvokeCommand: Nuget Behaviors 설치했습니다.

    * 보통 Click 이벤트 구현은 xaml.cs로 넘어가게 되는데, MVVM 패턴을 유지하기 위함입니다.

 

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
namespace ScrollButton.ViewModels
{
    public class ViewModelMain : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler? PropertyChanged;
 
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(propertyName));
        }
 
        #endregion
 
        #region Properties
        private ImageSource _selectedImageSource;
 
        public ImageSource SelectedImageSource
        {
            get { return _selectedImageSource; }
           set 
{
_selectedImageSource = value; 
OnPropertyChange(nameof(SelectedImageSource));
}
        }
 
        private int _SelectedImageSourceIndex;
 
        public int SelectedImageSourceIndex
        {
            get { return _SelectedImageSourceIndex; }
            set { _SelectedImageSourceIndex = value; }
        }
 
        private ObservableCollection<ImageSource> _imageSources = new ObservableCollection<ImageSource>();
 
        public ObservableCollection<ImageSource> ImageSources
        {
            get { return _imageSources; }
            set { _imageSources = value; }
        }
        #endregion
 
 
 
        #region Commands
        public RelayCommand ScrollImageCommand => null ?? new RelayCommand(ScrollImageEvent);
        #endregion
 
 
 
        #region Constructors
        public ViewModelMain()
        {
            ImageSources = new ObservableCollection<ImageSource>
            {
                new BitmapImage(new Uri("pack://application:,,,/Resources/number0.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number1.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number2.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number3.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number4.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number5.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number6.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number7.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number8.png"))
                , new BitmapImage(new Uri("pack://application:,,,/Resources/number9.png"))
            };
 
            SelectedImageSource = ImageSources[0];
        }
        #endregion
 
 
 
        #region Methods
        private void ScrollImageEvent(object obj)
        {
            if (obj is ListView listView)
            {
                var scrollViewer = FindScrollViewer(listView);
 
                if (scrollViewer != null)
                {
                    int middlePositionIndex = 2// 중앙에 위치시키고자 하는 인덱스
                    double newHorizontalOffset = SelectedImageSourceIndex - middlePositionIndex;
 
                    scrollViewer.ScrollToHorizontalOffset(newHorizontalOffset);
                    scrollViewer.UpdateLayout();
                }
            }
        }
 
        private ScrollViewer FindScrollViewer(DependencyObject dependencyObject)
        {
            if (dependencyObject is ScrollViewer)
            {
                return dependencyObject as ScrollViewer;
            }
 
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
            {
                var child = VisualTreeHelper.GetChild(dependencyObject, i);
                var scrollViewerInListView = FindScrollViewer(child);
                if (scrollViewerInListView != null)
                {
                    return scrollViewerInListView;
                }
            }
 
            return null;
        }
 
        #endregion
    }
}
 
 
 

 

🚨  문제가 ScrollViewer를 어떻게 얻어오냐..였습니다.

View와 ViewModel이 독립적이어야한다고 했지만, 도저히 방도가 생각나지 않아 CommandParameter로 ListView를 넘기고 ListView 하위에 있는 ScrollViewer를 찾아서 반환하는 형식으로 진행했습니다.

 

 

 

😮 오늘의 깨달음: Web의 Scroll Event Control과 프로그램의 Scroll Event Control은 다르다..!

'C# > WPF' 카테고리의 다른 글

[WPF] WebView2와 동영상  (0) 2024.04.13
[WPF] Button 클릭 영역과 Background  (0) 2024.04.06
[WPF] ListView와 SelectedItem 초기화  (0) 2024.03.31
[WPF] Converter와 Visibility  (0) 2024.03.18
[WPF] ResourceDictionary  (0) 2024.03.18