티스토리 뷰

반응형
어제에 이어서 바로 시작 하도록 하겠다.

1. 즐겨 찾기

즐겨 찾기 기능은 버스 노선과 정류소를 동시에 저장해서 즐겨 찾기 목록에 출력하고, 목록에서 하나를 선택하면, 해당 버스 노선의 정류소 목록이 조회가 되면서 동시에 해당 정류소 위치로 스크롤 이동이 되는 기능을 가진다. 이를 위해서, 버스 노선과 정류소 정보가 함께 들어가는 모델을 추가했으며, 정류소 목록에서 홀드(손가락으로 누르고 있는 동작)을 하면, ‘즐겨찾기 추가’ ContextMenu가 출력되어 선택하면, 메인 화면 하단부에 즐겨 찾기 목록이 바로 출력되고,(확인은 <-버튼을 눌러서 확인) 즐겨 찾기 목록에서 홀드시 ‘즐겨찾기 제거’ ContextMenu가 출력되어 즐겨 찾기 목록에서 제거가 되는 기능을 가진다.

2. 즐겨 찾기 모델 추가
FavoriteRow.cs를 추가하고 아래와 같이 코딩 한다.

using System.ComponentModel;
using System.Windows.Input;
 
namespace BusInfo.Models
{
    public class FavoriteRow : INotifyPropertyChanged
    {
        #region PropertyChange
        public event PropertyChangedEventHandler PropertyChanged;
 
        private void FirePropertyChange(string PropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }
        #endregion
 
        BusRouteModel bus;
        /// <summary>
        /// 버스 노선 정보
        /// </summary>
        public BusRouteModel Bus
        {
            get { return bus; }
            set
            {
                bus = value;
                FirePropertyChange("Bus");
            }
        }
 
        StationByRouteModel station;
        /// <summary>
        /// 정류소 정보
        /// </summary>
        public StationByRouteModel Station
        {
            get { return station; }
            set
            {
                station = value;
                FirePropertyChange("Station");
            }
        }
 
        /// <summary>
        /// 제거 커맨드
        /// </summary>
        public ICommand RemoveCommand { get; set; }
 
    }
}

////////////////////////////////////////////////////////////////////////////
//StationBusPosRow.cs
////////////////////////////////////////////////////////////////////////////

using System.ComponentModel;
using System.Windows.Input;
using BusInfo.ViewModels;
 
namespace BusInfo.Models
{
    public class StationBusPosRow : INotifyPropertyChanged
    {
        #region PropertyChange
        public event PropertyChangedEventHandler PropertyChanged;
 
        private void FirePropertyChange(string PropertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }
        #endregion
 
        StationByRouteModel station;
        /// <summary>
        /// 정류소 정보
        /// </summary>
        public StationByRouteModel Station
        {
            get { return station; }
            set
            {
                station = value;
                FirePropertyChange("Station");
            }
        }
 
        BusPosModel busPos;
        /// <summary>
        /// 버스 위치 정보
        /// </summary>
        public BusPosModel BusPos
        {
            get { return busPos; }
            set
            {
                busPos = value;
                FirePropertyChange("BusPos");
            }
        }
       
        /// <summary>
        /// 즐겨찾기 추가 커맨드
        /// </summary>
        public ICommand AddCommand { get; set; }
    }
}

이 모델에는 좀 특이하게 ICommand가 포함되었고, StationBusPosRow.cs에는 ICommand가 추가 되어 있다. 왜 ICommand를 추가해 놓았는지는 이 부분에 대한 설명은 뒤에서 하도록 하겠다.

3. 정류소 목록에서 ContextMenu 띄우고 '즐겨찾기 추가' 실행하기

StationDataTemplate의 Grid 영역 상단에 Toolkit에서 제공하는 기능인 ContextMenu를 추가한다.

<toolkit:ContextMenuService.ContextMenu>
    <toolkit:ContextMenu>
        <toolkit:MenuItem Header="즐겨찾기 추가" Command="{Binding AddCommand}" CommandParameter="{Binding Mode=OneWay}" />
    </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

이렇게 넣어 놓으면 Grid에서 사용자가 Hold를 하고 있으면, 자동으로 ContextMenu가 출력된다. 그리고, '즐겨찾기 추가'를 선택하면, AddCommand가 실행되면서 CommandParameter로 Binding된 한 줄의 데이터(여기서는 StationBusPosRow)를 가지고 간다.

4. AddCommand가 실행 되면서, 즐겨 찾기 목록에 데이터 추가하기

MainPageViewModel.cs에 AddCommand를 추가한다. 더불어 RemoveCommand도 함께 추가한다.

private ICommand addCommand;
/// <summary>
/// 즐겨찾기 추가
/// </summary>
public ICommand AddCommand
{
    get
    {
        if (addCommand == null)
        {
            addCommand = new ActionCommand(item =>
            {
                //CommandParameter로 받아온 아이템을 형변환한다.
                StationBusPosRow row = item as StationBusPosRow;
                //즐겨찾기 컬렉션에 추가한다. 새로 만들면서 현재 버스 정보, 선택된 row에 있는 정류소 정보를 함께 저장한다.
                //추가로 MainPageViewModel에 있는 RemoveCommand 링크도 함께 넣는다.
                FavoriteCollection
                    .Add(new FavoriteRow
                    {
                        Bus = BusCurrent,
                        Station = row.Station,
                        RemoveCommand = this.RemoveCommand
                    });
                //즐겨찾기 목록을 정렬한다.
                var sort = FavoriteCollection.OrderBy(p => p.Bus.BusRouteNm).ThenBy(p => p.Station.Station);
                FavoriteCollection = new ObservableCollection<FavoriteRow>(sort);
                MessageBox.Show("즐겨찾기에 추가 되었습니다.");
            });
        }
        return addCommand;
    }
}

private ICommand removeCommand;
/// <summary>
/// 즐겨찾기 제거
/// </summary>
public ICommand RemoveCommand
{
    get
    {
        if (removeCommand == null)
        {
            removeCommand = new ActionCommand(item =>
            {
                //즐겨찾기 아이템 하나를 형변환
                FavoriteRow row = item as FavoriteRow;
                if (row != null)
                {
                    //컬렉션에서 제거
                    FavoriteCollection.Remove(row);
                }
                MessageBox.Show("즐겨찾기에서 제거 되었습니다.");
            });
        }
        return removeCommand;
    }
}

5. 즐겨 찾기 컬렉션이 필요하다고 느끼고 있나? 바로 추가한다.
MainPageViewModel.cs에 FavoriteCollection를 추가한다.

ObservableCollection<FavoriteRow> favoriteCollection;

/// <summary>
/// 즐겨찾기 목록
/// </summary>
public ObservableCollection<FavoriteRow> FavoriteCollection
{
    get { return favoriteCollection; }
    set
    {
        favoriteCollection = value;
        FirePropertyChange("FavoriteCollection");
    }
}

FavoriteRow favoriteCurrent;
/// <summary>
/// 선택된 즐겨 찾기 프로퍼티
/// </summary>
public FavoriteRow FavoriteCurrent
{
    get { return favoriteCurrent; }
    set
    {
        favoriteCurrent = value;
        FirePropertyChange("FavoriteCurrent");
    }
}

6. 즐겨 찾기 컬렉션에 데이터가 들어 간다면, 목록을 확인 할 ListBox가 필요하다.
MainPage.xaml을 Blend로 수정하자

목표화면이을 참고해서 수정 하고, 최종 소스는 아래와 같다.
<phone:PhoneApplicationPage
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:BusInfo_ViewModels="clr-namespace:BusInfo.ViewModels"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:BusInfo_Converters="clr-namespace:BusInfo.Converters"
    x:Class="BusInfo.MainPage"
 xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">
 
 <phone:PhoneApplicationPage.Resources>
  <BusInfo_Converters:BoolToBoolRevConvert x:Key="BoolToBoolRevConvert"/>
 </phone:PhoneApplicationPage.Resources>
 <i:Interaction.Triggers>
  <i:EventTrigger>
   <i:InvokeCommandAction Command="{Binding LoadCommand, Mode=OneWay}"/>
  </i:EventTrigger>
 </i:Interaction.Triggers>
 
    <!--Sample code showing usage of ApplicationBar-->
    <!--<phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
            <shell:ApplicationBarIconButton IconUri="/Images/appbar_button1.png" Text="Button 1"/>
            <shell:ApplicationBarIconButton IconUri="/Images/appbar_button2.png" Text="Button 2"/>
            <shell:ApplicationBar.MenuItems>
                <shell:ApplicationBarMenuItem Text="MenuItem 1"/>
                <shell:ApplicationBarMenuItem Text="MenuItem 2"/>
            </shell:ApplicationBar.MenuItems>
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>-->

 <phone:PhoneApplicationPage.FontFamily>
  <StaticResource ResourceKey="PhoneFontFamilyNormal"/>
 </phone:PhoneApplicationPage.FontFamily>
 <phone:PhoneApplicationPage.FontSize>
  <StaticResource ResourceKey="PhoneFontSizeNormal"/>
 </phone:PhoneApplicationPage.FontSize>
 <phone:PhoneApplicationPage.Foreground>
  <StaticResource ResourceKey="PhoneForegroundBrush"/>
 </phone:PhoneApplicationPage.Foreground>

 <d:DataContext>
  <BusInfo_ViewModels:MainPageViewModel/>
 </d:DataContext>

    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="kaki104.tistory.com App" Style="{StaticResource PhoneTextNormalStyle}"/>
            <StackPanel Margin="9,0,0,0" Orientation="Horizontal">
             <TextBlock x:Name="PageTitle" Text="버스 목록 조회" Margin="0" Style="{StaticResource PhoneTextTitle1Style}" FontSize="48" Width="399" />
             <Image Height="48" Source="Icons/next.png" Stretch="Fill" Width="48" Margin="6,0,0,0">
              <i:Interaction.Triggers>
               <i:EventTrigger EventName="MouseLeftButtonDown">
                <i:InvokeCommandAction Command="{Binding NextCommand, Mode=OneWay}"/>
               </i:EventTrigger>
              </i:Interaction.Triggers>
             </Image>
            </StackPanel>
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
         <Grid.RowDefinitions>
          <RowDefinition Height="72"/>
          <RowDefinition/>
          <RowDefinition Height="72"/>
          <RowDefinition Height="Auto" MaxHeight="137"/>
         </Grid.RowDefinitions>
         <Border BorderThickness="0,2,0,0" BorderBrush="Gray">
          <StackPanel Orientation="Horizontal">
           <TextBlock TextWrapping="Wrap" Text="노선번호" VerticalAlignment="Center" FontSize="21.333" FontWeight="Bold"/>
           <TextBox TextWrapping="Wrap" Text="{Binding SBusNum, Mode=TwoWay}" Width="267" InputScope="Number"/>
           <Button Content="조회" IsEnabled="{Binding IsBusy, Converter={StaticResource BoolToBoolRevConvert}, Mode=OneWay}">
            <i:Interaction.Triggers>
             <i:EventTrigger EventName="Click">
              <i:InvokeCommandAction Command="{Binding GetBusRouteListCommand, Mode=OneWay}"/>
             </i:EventTrigger>
            </i:Interaction.Triggers>
           </Button>
          </StackPanel>
         </Border>
         <ListBox Grid.Row="1" x:Name="listBox" ItemTemplate="{StaticResource BusRouteDataTemplate}" ItemsSource="{Binding BusRouteCollection}">
          <i:Interaction.Triggers>
           <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand, Mode=OneWay}" CommandParameter="{Binding SelectedItem, ElementName=listBox, Mode=TwoWay}"/>
           </i:EventTrigger>
          </i:Interaction.Triggers>
         </ListBox>
         <Border Grid.Row="2" BorderBrush="Gray" BorderThickness="0,2,0,0">
          <StackPanel Orientation="Horizontal">
           <TextBlock TextWrapping="Wrap" Text="즐겨찾기" VerticalAlignment="Center" FontSize="21.333" FontWeight="Bold"/>
          </StackPanel>
         </Border>
         <ListBox x:Name="listBox1" Grid.Row="3" ItemsSource="{Binding FavoriteCollection}" ItemTemplate="{StaticResource FavoriteDataTemplate}" MaxHeight="136">
          <i:Interaction.Triggers>
           <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand, Mode=OneWay}" CommandParameter="{Binding SelectedItem, ElementName=listBox1, Mode=TwoWay}"/>
           </i:EventTrigger>
          </i:Interaction.Triggers>
         </ListBox>
        </Grid>
    </Grid>

</phone:PhoneApplicationPage>

7. ListBox에서 사용할 DataTemplate를 추가한다.
DataTemplate를 추가하는 방법을 다시 설명하지는 않는다. 이전 강좌를 참고

<DataTemplate x:Key="FavoriteDataTemplate">
 <Grid Width="450">
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="0.344*"/>
   <ColumnDefinition Width="0.656*"/>
  </Grid.ColumnDefinitions>

          <toolkit:ContextMenuService.ContextMenu>
              <toolkit:ContextMenu IsZoomEnabled="False">
                  <toolkit:MenuItem Header="즐겨찾기 제거" Command="{Binding RemoveCommand}" CommandParameter="{Binding Mode=OneWay}" />
              </toolkit:ContextMenu>
          </toolkit:ContextMenuService.ContextMenu>

  <StackPanel Orientation="Horizontal" Margin="0">
   <Border BorderThickness="10,0,0,0" BorderBrush="{Binding Bus.RouteType, Converter={StaticResource RouteTypeToColorConverter}}" Margin="0,4">
    <TextBlock TextWrapping="Wrap" Text="{Binding Bus.BusRouteNm}" Height="27" />
   </Border>
  </StackPanel>
  <StackPanel Orientation="Horizontal" Grid.Column="1" Margin="0">
   <TextBlock TextWrapping="Wrap" Text="{Binding Station.StationNm}" Margin="10,4,0,4"/>
  </StackPanel>
 </Grid>
</DataTemplate>

즐겨 찾기 목록에 있는 ContextMenu는 '즐겨찾기 제거'라는 헤더를 가지고 있음을 알 수 있다. 그리고, 메뉴를 선택하면 RemoveCommand를 실행하고, CommandParameter는 자기 자신을 가지고 간다는 것을 알 수 있다.

8. 즐겨 찾기 목록에서 하나를 선택하면, 전에 사용했던 SelectionChangedCommand를 실행 한다.

private ICommand selectionChangedCommand;
/// <summary>
/// 노선 목록에서 선택된 아이템이 변경된 경우 실행되는 커맨드
/// </summary>
public ICommand SelectionChangedCommand
{
    get
    {
        if (selectionChangedCommand == null)
        {
            selectionChangedCommand = new ActionCommand(item =>
            {
                //WC가 바쁠때나 item이 null일 때는 건들이지 않는다,
                if (wc.IsBusy == true || item == null)
                    return;

                //item의 형이름을 가지고 구분해서 처리한다.
                BusRouteModel route;
                switch (item.GetType().Name)
                {
                    case "BusRouteModel":
                        //커맨드 파라메터로 전달 받은 오브젝트를 형변환
                        route = item as BusRouteModel;
                        //현재 선택된 즐겨 찾기 지운다.
                        FavoriteCurrent = null;
                        break;
                    case "FavoriteRow":
                        //형변환
                        route = (item as FavoriteRow).Bus;
                        //즐겨 찾기 선택
                        FavoriteCurrent = item as FavoriteRow;
                        break;
                    default:
                        route = null;
                        break;
                }
                //형변환을 성공적으로 처리했다면
                if (route != null)
                {
                    //로딩 화면 출력
                    Splash.IsOpen = true;
                    //선택된 버스 정보 저장
                    BusCurrent = route;

                    //정류소 조회
                    var uri = new Uri("http://ws.bus.go.kr/api/rest/busRouteInfo/getStaionByRoute?ServiceKey=" + SKey.BusRouteInfo + "&busRouteId=" + route.BusRouteId);
                    wc.DownloadStringAsync(uri, "GetStaionByRoute");

                    //던지고 바로 정류소 목록 조회 화면으로 네비게이션
                    root.Navigate(new Uri("/Views/StationByRouteView.xaml", UriKind.Relative));
                }
            });
        }
        return selectionChangedCommand;
    }
}

9.
정류소 목록이 조회 된 후에 즐겨 찾기에 지정된 정류소로 ListBox 스크롤 시키기
ListBox에서 특정 위치로 Scroll을 시키기 위해서는 일단 컬렉션을 변경(이 작업은 ViewModel에서)해서 목록을 바꿔주고, 바꾸어진 목록을 인식 시킨 후에 ListBox의 명령을 실행해서 이동(이 작업은 View에서)을 해야한다.

1) ViewModel(MainPageViewModel.cs) 작업
RowCollection = new ObservableCollection<StationBusPosRow>(SBProws); //이 문장을 찾아서 바로 아래 추가한다.

 // 정류소 목록 바인딩 완료 되었다는 이벤트를 강제로 발생

FirePropertyChange("RowCollectionChanged");

2) View(StationByRouteView.xaml.cs) 작업
//DataContext에 입력 <- 이 문장을 찾아서 바로 아래 추가한다.
this.DataContext = ViewModel;

//뷰모델의 프로퍼티 체인지 이벤트 추가
ViewModel.PropertyChanged += new PropertyChangedEventHandler(ViewModel_PropertyChanged);
...
...
/// <summary>
///  뷰모델 프로퍼티 체인지 이벤트 구현
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "RowCollectionChanged":
            //정류소 목록 구성 완료 이벤트 처리
            if (ViewModel.FavoriteCurrent != null)
            {
                //리스트 박스에 아이템 하나를 선택하고,
                listBox.SelectedValue = ViewModel.FavoriteCurrent.Station.Station;
                //리스트 박스 내용 한번 확인 시키고
                listBox.UpdateLayout();
                //리스트 박스에 선택된 아이템으로 화면 이동
                listBox.ScrollIntoView(listBox.SelectedItem);
            }
            break;
    }
}

10. 즐겨 찾기 모델과 정류소&위치 모델에 ICommand가 존재하는 이유
여기까지 하면 즐겨 찾기 기능은 어느 정도 구현한 것 같다. 여기서, ICommand를 모델에 추가한 이유를 설명하도록 하겠다. 혹시, DataTemplate 내부에서 ViewModel의 ICommand를 실행 시켜 본 적이 있는가? 실행을 시켜 보았다면, 이 부분은 넘어가도 좋다.

그럼 그 이유를 설명 하자면, 실버라이트 4.0 기준으로 DataTemplate에서 ViewModel에 접근 하는 것 자체가 매우 어렵기 때문이다. 생각을 해보면, 우리는 ViewModel을 View의 DataContext에 Binding을 해서 사용하는데, 그렇게 되면 ListBox의 DataContext도 상위의 DataContext를 상속을 받게 된다. 그래서 ListBox의 ItemsSource에 컬렉션을 Binding할 수 있게 되는 것인데, DataTemplate은 ListBox의 내부의 하나의 Item을 표현하는 곳이기 때문에 그 상위로는 접근을 할 수가 없게 되어 있다. 이 부분은 실버라이트 5.0 부터는 해결이 되어 있지만, Windows Phone 7.1 Mango 버전은 아직 4.0을 사용하고 있기 때문에 다른 방법을 사용해야 하는 것이다.

두가지 방법이 있는데 첫번째는 ViewModel을 Static 변수에 담아두고 DataTemplate에서도 접근이 가능하게 하는 방법이 있고, 다른 하나는 위에서 사용한 방법 처럼 Model에 ICommand를 포함 시키고, 그 ICommand를 DataTemplate에서 사용 하는 방법이다. 첫 번째 방법은 외국의 유명한 회사에서 제안한 방법으로 알고 있으며, 두번 째 방법은 혼자 삽질하다가 터득한 방법인데, 단점 이라면, 나중에 격리 저장소에 저장 할때 ICommand에 연결된 Link를 끊어 주고, 불러와서 다시 연결을 해 줘야 하는 것이다.(처음 방법은 이런 작업은 필요 없을 것이라는 생각이 든다.)

다음 편을 바로 작성 하도록 하겠다.

반응형
댓글