2016-07-18 2 views
0

저는 WPF를 가르칩니다. 내 창에는 두 개의 콤보 상자가 있습니다. 하나는 범주는이고 다른 하나는 하위 범주입니다. 범주 선택이 변경되면 하위 범주 목록이 선택한 범주에있는 범주만으로 업데이트됩니다.WPF 뷰 모델의 올바른 사용

두 콤보 상자 모두에 대해 간단한보기 클래스를 만들었습니다. 내 SubcategoryView 클래스의 생성자는 내 CategoryView 클래스에 대한 참조를 취해 범주 선택이 변경 될 때 이벤트 핸들러를 연결합니다.

public class SubcategoryView : INotifyPropertyChanged 
{ 
    protected CategoryView CategoryView; 

    public SubcategoryView(CategoryView categoryView) 
    { 
     CategoryView = categoryView; 
     CategoryView.PropertyChanged += CategoryView_PropertyChanged; 
    } 

    private void CategoryView_PropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName == "SelectedItem") 
     { 
      _itemsSource = null; 
     } 
    } 

    private ObservableCollection<TextValuePair> _itemsSource; 
    public ObservableCollection<TextValuePair> ItemsSource 
    { 
     get 
     { 
      if (_itemsSource == null) 
      { 
       // Populate _itemsSource 
      } 
      return _itemsSource; 
     } 
    } 
} 

DataContext은 다음과 같이 지정합니다.

cboCategory.DataContext = new CategoryView(); 
cboSubcategory.DataContext = new SubcategoryView(cboCategory.DataContext as CategoryView); 

문제는 (내 PropertyChanged 핸들러가 호출되고 확인하더라도) 내 카테고리 콤보 상자에 새 항목을 선택하면 하위 범주가 다시 채울 발생하지 않는다는 것입니다.

목록을 다시 채우게하는 올바른 방법은 무엇입니까?

또한이 접근법에 대한 다른 의견도 환영합니다. 내 CategoryView을 생성자에 전달하는 대신 XAML에 선언적으로 표시하는 것이 더 낫습니까?

+0

이 제품을 살펴 보았습니까? http://stackoverflow.com/questions/9185366/mvvm-wpf-master-detail-comboboxes –

+0

나는 그것을 보는 방법, 당신은 모든 일을 잘못하고있다. @SreeHarshaNellore가 준 링크를 참조하십시오. 필요한 것은 단일보기 (두 개의 콤보 상자 포함)와 단일보기 모델입니다. – Jai

+0

나는 각 카테고리에 하위 카테고리 하위 카테고리의 컬렉션을 제공 할 것입니다. x : Category 콤보 CategorySelector의 이름을 지정합니다. 다른 콤보의 ItemsSource = "{Binding SelectedItem.SubCategories, ElementName = CategorySelector}"을 바인딩하십시오. 밥은 당신 삼촌이야. –

답변

3

가있다 :

Wrapper.DataContext = new CommonViewModel(); 

그리고 코드 BindableBase에 대한 암호.

각 카테고리는 하위 카테고리가 무엇인지 알고 있습니다. 데이터베이스 나 디스크 파일, 데이터베이스/웹 서비스 메서드/파일 판독기/클래스를 반환하면 클래스가 반환되며 일치하도록 뷰 모델을 만들 수 있습니다. 뷰 모델은 정보의 구조를 이해하지만 실제 내용에 대해 아무 것도 모른다. 다른 누군가가 그 일을 담당하고 있습니다.

이것은 모두 매우 선언적입니다. 유일한 루프는 데모 개체를 가짜로 만듭니다. 이벤트 핸들러는 없으며, 뷰 모델을 생성하고 가짜 데이터로 자신을 채우라는 것을 제외하고는 코드 숨김에 아무것도 없습니다. 실생활에서 특별한 경우를 위해 이벤트 핸들러를 작성하는 경우가 종종 있습니다 (예 : 드래그 앤 드롭). 코드 비하인드에 뷰 특정 로직을 배치하는 것에 관한 MVVM 이외의 것은 없습니다. 그게 거기있는거야. 그러나이 경우는 그렇게하기에는 너무 사소한 것입니다. 우리는 정확히 수년간 TFS에 앉아 있던 파일이 마법사가 만든대로 정확히 .xaml.cs 개의 파일을 가지고 있습니다.

viewmodel 속성은 많은 상용구입니다. 나는 #regions와 모든 것들을 생성하기 위해 미리보기 (steal them here)를 가지고있다. 다른 사람들이 복사하여 붙여 넣기.

일반적으로 각 viewmodel 클래스는 별도의 파일에 저장하지만 예제 코드입니다.

C# 6 용으로 작성되었습니다. 이전 버전을 사용하고 계시다면 알려 주시기 바랍니다.

마지막으로, 하나의 콤보 상자 (또는 무엇이든)가 트리를 탐색하는 대신 다른 큰 항목 모음을 필터링한다는 측면에서 생각하는 것이 더 합당한 경우가 있습니다. 특히 "카테고리": "하위 카테고리"관계가 일대 다가 아닌 경우,이 계층 구조 형식으로이를 수행하는 것이 거의 불가능합니다.

이 경우 우리는 "categories"컬렉션과 컬렉션 모두 "하위 범주"를 모두 기본보기 모델의 속성으로 사용하게됩니다. 그런 다음 "카테고리"선택을 사용하여 일반적으로 CollectionViewSource을 통해 "하위 카테고리"컬렉션을 필터링합니다. 그러나 뷰 모델에 ReadOnlyObservableCollection (FilteredSubCategories)과 같은 두 번째 콤보 상자를 바인딩하는 모든 "하위 범주"의 개인 전체 목록을 제공 할 수도 있습니다. '카테고리'선택이 변경되면 SelectedCategory을 기반으로 FilteredSubCategories을 다시 채 웁니다.

결론은 데이터의 의미를 반영하는보기 모델을 작성한 다음 사용자가보고해야 할 것을보고 수행해야 할 작업을 수행 할 수 있도록보기를 작성하는 것입니다. 뷰 모델은 뷰가 존재한다는 것을 인식해서는 안됩니다. 그들은 단지 정보와 명령을 폭로합니다. 동일한 뷰 모델을 다른 방법이나 다른 세부 수준으로 표시하는 여러 뷰를 작성할 수 있으므로 뷰 모델은 누구나 사용할 수있는 자체에 대한 정보를 중립적으로 노출하는 것으로 생각하면 편리합니다. 보통 인수 분해 규칙이 적용됩니다 : 커플을 느슨하게 (더 이상 느슨하게하지만) 가능한 한 등

ComboDemoViewModels.cs

using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.ComponentModel; 
using System.Linq; 
using System.Runtime.CompilerServices; 
using System.Text; 
using System.Threading.Tasks; 

namespace ComboDemo.ViewModels 
{ 
    public class ViewModelBase : INotifyPropertyChanged 
    { 
     #region INotifyPropertyChanged 
     public event PropertyChangedEventHandler PropertyChanged; 

     protected void OnPropertyChanged([CallerMemberName] String propName = null) 
     { 
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); 
     } 
     #endregion INotifyPropertyChanged 
    } 

    public class ComboDemoViewModel : ViewModelBase 
    { 
     // In practice this would probably have a public (or maybe protected) setter 
     // that raised PropertyChanged just like the other properties below. 
     public ObservableCollection<CategoryViewModel> Categories { get; } 
      = new ObservableCollection<CategoryViewModel>(); 

     #region SelectedCategory Property 
     private CategoryViewModel _selectedCategory = default(CategoryViewModel); 
     public CategoryViewModel SelectedCategory 
     { 
      get { return _selectedCategory; } 
      set 
      { 
       if (value != _selectedCategory) 
       { 
        _selectedCategory = value; 
        OnPropertyChanged(); 
       } 
      } 
     } 
     #endregion SelectedCategory Property 

     public void Populate() 
     { 
      #region Fake Data 
      foreach (var x in Enumerable.Range(0, 5)) 
      { 
       var ctg = new ViewModels.CategoryViewModel($"Category {x}"); 

       Categories.Add(ctg); 

       foreach (var y in Enumerable.Range(0, 5)) 
       { 
        ctg.SubCategories.Add(new ViewModels.SubCategoryViewModel($"Sub-Category {x}/{y}")); 
       } 
      } 
      #endregion Fake Data 
     } 
    } 

    public class CategoryViewModel : ViewModelBase 
    { 
     public CategoryViewModel(String name) 
     { 
      Name = name; 
     } 

     public ObservableCollection<SubCategoryViewModel> SubCategories { get; } 
      = new ObservableCollection<SubCategoryViewModel>(); 

     #region Name Property 
     private String _name = default(String); 
     public String Name 
     { 
      get { return _name; } 
      set 
      { 
       if (value != _name) 
       { 
        _name = value; 
        OnPropertyChanged(); 
       } 
      } 
     } 
     #endregion Name Property 

     // You could put this on the main viewmodel instead if you wanted to, but this way, 
     // when the user returns to a category, his last selection is still there. 
     #region SelectedSubCategory Property 
     private SubCategoryViewModel _selectedSubCategory = default(SubCategoryViewModel); 
     public SubCategoryViewModel SelectedSubCategory 
     { 
      get { return _selectedSubCategory; } 
      set 
      { 
       if (value != _selectedSubCategory) 
       { 
        _selectedSubCategory = value; 
        OnPropertyChanged(); 
       } 
      } 
     } 
     #endregion SelectedSubCategory Property 
    } 

    public class SubCategoryViewModel : ViewModelBase 
    { 
     public SubCategoryViewModel(String name) 
     { 
      Name = name; 
     } 

     #region Name Property 
     private String _name = default(String); 
     public String Name 
     { 
      get { return _name; } 
      set 
      { 
       if (value != _name) 
       { 
        _name = value; 
        OnPropertyChanged(); 
       } 
      } 
     } 
     #endregion Name Property 
    } 
} 

MainWindow.xaml에게

<Window 
    x:Class="ComboDemo.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:local="clr-namespace:ComboDemo" 
    mc:Ignorable="d" 
    Title="MainWindow" Height="350" Width="525"> 
    <Grid> 
     <StackPanel Orientation="Vertical" Margin="4"> 
      <StackPanel Orientation="Horizontal"> 
       <Label>Categories</Label> 
       <ComboBox 
        x:Name="CategorySelector" 
        ItemsSource="{Binding Categories}" 
        SelectedItem="{Binding SelectedCategory}" 
        DisplayMemberPath="Name" 
        MinWidth="200" 
        /> 
      </StackPanel> 
      <StackPanel Orientation="Horizontal" Margin="20,4,4,4"> 
       <Label>Sub-Categories</Label> 
       <ComboBox 
        ItemsSource="{Binding SelectedCategory.SubCategories}" 
        SelectedItem="{Binding SelectedCategory.SelectedSubCategory}" 
        DisplayMemberPath="Name" 
        MinWidth="200" 
        /> 
      </StackPanel> 
     </StackPanel> 
    </Grid> 
</Window> 

MainWindow.xaml.cs를을

using System.Windows; 

namespace ComboDemo 
{ 
    /// <summary> 
    /// Interaction logic for MainWindow.xaml 
    /// </summary> 
    public partial class MainWindow : Window 
    { 
     public MainWindow() 
     { 
      InitializeComponent(); 

      var vm = new ViewModels.ComboDemoViewModel(); 

      vm.Populate(); 

      DataContext = vm; 
     } 
    } 
} 

추가 신용

다음은 MainWindow의 다른 버전입니다.xaml : 두 가지 방법으로 동일한보기 모델을 표시하는 방법을 보여줍니다. 한 목록에서 범주를 선택하면 SelectedCategory이 업데이트되어 다른 목록에 반영되고 같은 내용이 SelectedCategory.SelectedSubCategory에 해당합니다.

<Window 
    x:Class="ComboDemo.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:local="clr-namespace:ComboDemo" 
    xmlns:vm="clr-namespace:ComboDemo.ViewModels" 
    mc:Ignorable="d" 
    Title="MainWindow" Height="350" Width="525" 
    > 
    <Window.Resources> 
     <DataTemplate x:Key="DataTemplateExample" DataType="{x:Type vm:ComboDemoViewModel}"> 
      <ListBox 
       ItemsSource="{Binding Categories}" 
       SelectedItem="{Binding SelectedCategory}" 
       > 
       <ListBox.ItemTemplate> 
        <DataTemplate DataType="{x:Type vm:CategoryViewModel}"> 
         <StackPanel Orientation="Horizontal" Margin="2"> 
          <Label Width="120" Content="{Binding Name}" /> 
          <ComboBox 
           ItemsSource="{Binding SubCategories}" 
           SelectedItem="{Binding SelectedSubCategory}" 
           DisplayMemberPath="Name" 
           MinWidth="120" 
           /> 
         </StackPanel> 
        </DataTemplate> 
       </ListBox.ItemTemplate> 
      </ListBox> 
     </DataTemplate> 
    </Window.Resources> 

    <Grid> 
     <StackPanel Orientation="Vertical" Margin="4"> 
      <StackPanel Orientation="Horizontal"> 
       <Label>Categories</Label> 
       <ComboBox 
        x:Name="CategorySelector" 
        ItemsSource="{Binding Categories}" 
        SelectedItem="{Binding SelectedCategory}" 
        DisplayMemberPath="Name" 
        MinWidth="200" 
        /> 
      </StackPanel> 
      <StackPanel Orientation="Horizontal" Margin="20,4,4,4"> 
       <Label> 
        <TextBlock Text="{Binding SelectedCategory.Name, StringFormat='Sub-Categories in {0}:', FallbackValue='Sub-Categories:'}"/> 
       </Label> 
       <ComboBox 
        ItemsSource="{Binding SelectedCategory.SubCategories}" 
        SelectedItem="{Binding SelectedCategory.SelectedSubCategory}" 
        DisplayMemberPath="Name" 
        MinWidth="200" 
        /> 
      </StackPanel> 

      <GroupBox Header="Another View of the Same Thing" Margin="4"> 
       <!-- 
       Plain {Binding} just passes along the DataContext, so the 
       Content of this ContentControl will be the main viewmodel. 
       --> 
       <ContentControl 
        ContentTemplate="{StaticResource DataTemplateExample}" 
        Content="{Binding}" 
        /> 
      </GroupBox> 
     </StackPanel> 
    </Grid> 
</Window> 
+0

자세한 답변을 보내 주셔서 감사합니다. 불행히도, 나는 현재 다른 프로젝트에 집중하고 있지만 게시 한 모든 내용을 매우 신중하게 검토 할 것입니다. –

1

이 경우 단일보기 모델을 사용하면 주석에서 언급 한 것처럼 실제로는 더 간단합니다. 예를 들어 콤보 상자 항목에는 문자열 만 사용합니다.

뷰 모델을 올바르게 사용하기 위해 UI 이벤트가 아닌 바인딩을 통해 카테고리의 변경 사항을 추적합니다. 따라서 ObservableCollection 외에도 SelectedCategory 속성이 필요합니다.

보기 - 모델 :

public class CommonViewModel : BindableBase 
{ 
    private string selectedCategory; 

    public string SelectedCategory 
    { 
     get { return this.selectedCategory; } 
     set 
     { 
      if (this.SetProperty(ref this.selectedCategory, value)) 
      { 
       if (value.Equals("Category1")) 
       { 
        this.SubCategories.Clear(); 
        this.SubCategories.Add("Category1 Sub1"); 
        this.SubCategories.Add("Category1 Sub2"); 
       } 

       if (value.Equals("Category2")) 
       { 
        this.SubCategories.Clear(); 
        this.SubCategories.Add("Category2 Sub1"); 
        this.SubCategories.Add("Category2 Sub2"); 
       } 
      } 
     } 
    } 

    public ObservableCollection<string> Categories { get; set; } = new ObservableCollection<string> { "Category1", "Category2" }; 

    public ObservableCollection<string> SubCategories { get; set; } = new ObservableCollection<string>(); 
} 

SetPropertyINotifyPropertyChanged의 구현입니다.

당신은 카테고리, SelectedCategory 속성 트리거의 세터를 선택하고 선택한 카테고리 값에 따라 subcatagory 항목을 입력 할 수 있습니다합니다. 컬렉션 개체 자체를 바꾸지 마십시오! 기존 항목을 지우고 새 항목을 추가해야합니다. XAML에서

ItemsSource 외에 두 콤보 상자를 들어, 카테고리 콤보 상자 SelectedItem 결박해야합니다.

XAML :

<StackPanel x:Name="Wrapper"> 
    <ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=OneWayToSource}" /> 
    <ComboBox ItemsSource="{Binding SubCategories}" /> 
</StackPanel> 

그럼 그냥 래퍼의 데이터 컨텍스트에 뷰 - 모델을 할당 : 우리가 생산에 그것을 할 방법

다음
using System.ComponentModel; 
using System.Runtime.CompilerServices; 

public abstract class BindableBase : INotifyPropertyChanged 
{ 
    public event PropertyChangedEventHandler PropertyChanged; 

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) 
    { 
     if (Equals(storage, value)) 
     { 
      return false; 
     } 

     storage = value; 
     this.OnPropertyChanged(propertyName); 
     return true; 
    } 

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    { 
     this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 
+0

이것은 작동 코드이지만 MVVM이 아닙니다. –

+0

@EdPlunkett 당신은 eleborate 수 있습니까? – Sam

+0

"MVVM이 아닙니다." Add 항목은 코드 숨김에 ComboBoxItem을 추가하는 것보다 훨씬 낫습니다. 그러나 각 카테고리에 자체 하위 카테고리 모음이 있고 선택한 카테고리의 하위 카테고리 속성에 바인딩하는 경우 코드가 간단 해집니다. 또한 모든 하위 범주의 컬렉션을 하나씩 가지고 CollectionViewSource로 필터링 할 수 있습니다. . –

관련 문제