ساختار درختی Cancelable در WPF/MVVM

در این مقاله بعد از توضیح مختصری راجع به WPF یک نمونه ای که در آن ساختار Tree View وجود دارد پیاده سازی می کنیم .در این Tree view امکان انتخاب و cancel کردن برای کاربر در نظر گرفته شده است .

ساختار درختی Cancelable  در WPF/MVVM

همان طور که می دانید با استفاده از دات نت برنامه های بسیاری می توان طراحی کرد .یک نوع از این Application ها برنامه های ویندوزی بودند که تا سالیان زیادی جوابگوی نیاز های مختلف بودند .ولی ایرادی که به این برنامه های وارد بود ، این بود که گرافیک ضعیفی داشتند و برای داشتن همان گرافیک ضعیف هم وابسته به API ویندوز بودند . در کنترل هایی که خودمان هم ایجاد می کردیم و از کنترل های دات نت ارث بری می کردیم هم نمی توانستیم تغییرات زیادی اعمال کنیم .

مشکل دیگری که وجود داشت ترکیب دو قسمت کد و طراحی بود .ولی در WPF این دو قسمت از هم جدا شده اند .با کمک wcf یک گرافیک زیبا و چشم گیر برای برنامه های ویندوزی خود خواهیم داشت .

سابقا در برنامه های ویندوزی برای گرافیکی که در برنامه به کار برده بودیم نیازمند دو بخش از سیستم عامل به نام های User32 و GDI/GDI داریم .بعد ها برای رفع کمبودهایی که این دو کتابخانه داشتند مایکروسافت کتابخانه جدیدی به نام DirectX ارائه داد.در wpf از تکنولوژی GDI/GDI  به عنوان پایه کار استفاده می شود.

همان طور که می دانید wpf از معماری MVVM استفاده می کند و تفاوت آن با مدل MVC این است که در آن controller با View Model جانشین شده است .در مدل mvc کنترلر تصمیم می گیرد که چه مدلی به view نمایش دهد ولی در MVVM هر عنصر به یک مدل موجود در ViewModel (معرفی و مزایای استفاده از view Model در MVC)مرتبط شده است و این ویو است که تصمیم می گیرد از کدام اطلاعات موجود در ViewModel بهره گیرد.
بعد از معرفی کوتاه تکنولوژی wpf در این مقاله امکان جدیدی به نام Cancelable TreeView Navigation را معرفی میکنیم .
اگر برنامه را اجرا کنید شکل زیر را خواهید دید

شکل به صورت کلی مفهوم Tree view و select و یا Cancel کردن شاخه ها را نشان می دهد.
ابتدا در داخل DocumentViewModelBase یک property به نام isDirty تعریف می کنیم .

public bool IsDirty
{
  get
  {
    return _IsDirty;
  }

  set
  {
    if (_IsDirty != value)
    {
      if (DirtyFlagChangedEvent != null)
        DirtyFlagChangedEvent(this, new DocumentDirtyChangedEventArgs(_IsDirty, value));

      _IsDirty = value;
      RaisePropertyChanged(() => IsDirty);
    }
  }
}


هر تغییری در این property باعث اجرا شدن یک رویداد به نام DocumentViewModelBase.DirtyFlagChangedEvent می شود.که این رویداد در متد ApplicationViewModel.CurrentDocument_DirtyFlagChangedEvent اجرا می شود.

private void CurrentDocument_DirtyFlagChangedEvent(object sender, DocumentDirtyChangedEventArgs e)
{
  this.CancelTreeVieSelection = e.IsDirtyNewValue;
}

در این طراحی از یک property عمومی به نام CancelTreeVieSelection استفاده کرده ایم که مشخص می کند که آیا انتخابی صورت گرفته است یا نه .این property می تواند پراپرتی document IsDirty را زمانی که انتخاب جدیدی صورت گرفت بازنویسی کند .
زمانی که کاربر از ساختار درختی موردی را انتخاب کرده باشد در حالی که تیک گزینه Cancel Selection را زده باشد خطای زیر را خواهد دید.

بعد از زدن گزینه yes باید در ساختار درختی گزینه های دیگری را انتخاب کند .
viewModels های Document Views
document views در این کلاس توسط کلاس های View Model کنترل می شود.که کلاس های view model شامل RootViewModel و یا DocumentViewModel است .هر دو این کلاس ها از کلاس DocumentViewModelBase ارث بری میکنند.

رویداد DocumentDirtyChangedEventArgs توسط کلاس DocumentViewModelBase پیاده سازی می شود
در کد زیر ساختار MainWindow را می بینید

<Grid Grid.Row="2">
  <Grid.Resources>
      <!--
      These datatemplates map a type of viewmodel into their view.
      This map definition is used below in the ContentPresenter to
      show the correct page for each type of view.
      -->
      <DataTemplate DataType="{x:Type vm:DocumentViewModel}">
        <StackPanel Margin="3">
          <TextBlock Text="This is a Document View!"  Margin="3"/>
          <TextBlock Text="{Binding DocumentTitle, StringFormat={} Document Title:{0}}"  Margin="3"/>
          <CheckBox Content="Document IsDirty" IsChecked="{Binding IsDirty}"  Margin="3"/>
        </StackPanel>
      </DataTemplate>
      <DataTemplate DataType="{x:Type vm:RootViewModel}">
        <StackPanel Margin="3">
          <TextBlock Text="This is a ROOT View!"  Margin="3"/>
            <TextBlock Text="{Binding DocumentTitle, StringFormat={} Document Title:{0}}"  Margin="3"/>
            <CheckBox Content="ROOT view document IsDirty" IsChecked="{Binding IsDirty}"  Margin="3"/>
        </StackPanel>
      </DataTemplate>

  </Grid.Resources>
  <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <TreeView Grid.Column="0"
            ItemsSource="{Binding TreeViewItems}"
            behav:TreeViewSelectionChangedBehavior.UndoSelection="{Binding CancelTreeVieSelection}"
            behav:TreeViewSelectionChangedBehavior.ChangedCommand="{Binding SelectItemChangedCommand}"
             >
    <TreeView.ItemTemplate>
      <HierarchicalDataTemplate ItemsSource="{Binding Children}">
         <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Name}"
                        ToolTipService.ShowOnDisabled="True"
                        VerticalAlignment="Center" Margin="3" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </Style>
    </TreeView.ItemContainerStyle>
  </TreeView>

  <ContentControl Grid.Column="1" Content="{Binding CurrentDocument}" Margin="3" />
</Grid>


کد بالا ساختار شکلی که در ابتدای مقاله دیدید را تشریح می کند.متد uiElement_PreviewMouseDown که درون TreeViewSelectionChangedBehavior وجود دارد زمانی اجرا خواهد شد که پراپرتی UndoSelection مقدار true داشته باشد.
 

private static void uiElement_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
  // first did the user click on a tree node?
  var source = e.OriginalSource as DependencyObject;
  while (source != null && !(source is TreeViewItem))
      source = VisualTreeHelper.GetParent(source);

  var itemSource = source as TreeViewItem;
  if (itemSource == null)
      return;

  var treeView = sender as TreeView;
  if (treeView == null)
      return;

  bool undoSelection = TreeViewSelectionChangedBehavior.GetUndoSelection(treeView);
  if (undoSelection == false)
      return;

  // Cancel the attempt to select an item.
  var result = MessageBox.Show("The current document has unsaved data. Do you want to continue without saving data?", "Are you really sure?",
                               MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);

  if (result == MessageBoxResult.No)
  {
    // Cancel the attempt to select a differnet item.
    e.Handled = true;
  }
  else
  {
    // Lets disable this for a moment, otherwise, we'll get into an event "recursion"
    treeView.PreviewMouseDown -= uiElement_PreviewMouseDown;

    // Select the new item - make sure a SelectedItemChanged event is fired in any case
    // Even if this means that we have to deselect/select the one and the same item
    if (itemSource.IsSelected == true )
        itemSource.IsSelected = false;

    itemSource.IsSelected = true;

    // Lets enable this to get back to business for next selection
    treeView.PreviewMouseDown += uiElement_PreviewMouseDown;
  }
}

کد زیر مربوط به پراپرتی SelectItemChangedCommand می باشد

public ICommand SelectItemChangedCommand
{
  get
  {
    if (_SelectItemChangedCommand == null)
    {
      _SelectItemChangedCommand = new RelayCommand<object>((p) =>
      {
        var param = p as ItemViewModel;
    
        if (param != null)
        {
          this.SelectedTreeViewItem = param;
    
          if (this.CurrentDocument != null)
             this.CurrentDocument.DirtyFlagChangedEvent -= CurrentDocument_DirtyFlagChangedEvent;
    
          if (param.Name == "Root")
          {
              this.CurrentDocument = new RootViewModel();
          }
          else
             this.CurrentDocument = new DocumentViewModel();
    
          this.CurrentDocument.DocumentTitle = param.Name;
          this.CancelTreeVieSelection = this.CurrentDocument.IsDirty;
          this.CurrentDocument.DirtyFlagChangedEvent += CurrentDocument_DirtyFlagChangedEvent;
        }
      });
    }
    
    return _SelectItemChangedCommand;
  }
}

 

فایل های ضمیمه
دانلود نسخه ی PDF این مطلب