2013-04-10 16 views
21

Obecnie próbuję osiągnąć funkcjonalność tab-control z ukrytymi zakładkami przy użyciu ListView (jako zakładki) i ContentControl z powiązaniem właściwości treści.Binding ContentControl Zawartość dla zawartości dynamicznej

czytałem trochę na ten temat i jeśli mam rację, to powinno działać w ten sposób:

<Grid> 
    <Grid.ColumnDefinitions> 
     <ColumnDefinition Width="20.0*"/> 
     <ColumnDefinition Width="80.0*"/> 
    </Grid.ColumnDefinitions> 
    <ListBox Grid.Column="0"> 
     <ListBoxItem Content="Appearance"/> 
    </ListBox> 

    <ContentControl Content="{Binding SettingsPage}" Grid.Column="1"/> 
</Grid> 
. 
. 
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
    <ContentControl x:Key="AppearancePage"> 
     <TextBlock Text="Test" /> 
    </ContentControl> 
    <ContentControl x:Key="AdvancedPage"> 
     <TextBlock Text="Test2" /> 
    </ContentControl> 
</ResourceDictionary> 

I w tył kodu:

public partial class MainWindow : MetroWindow 
    { 
    private ContentControl SettingsPage; 
    private ResourceDictionary SettingsPagesDict = new ResourceDictionary(); 

    public MainWindow() 
    { 
     InitializeComponent(); 

     SettingsPagesDict.Source = new Uri("SettingsPages.xaml", UriKind.RelativeOrAbsolute); 
     SettingsPage = SettingsPagesDict["AppearancePage"] as ContentControl; 

Allthough zgłasza żadnego błędu, nie wyświetla "TextBlock" testu.

Jest prawdopodobne, że mam koncepcję wiązania błędnego, proszę dać mi wskazówkę we właściwym kierunku.

Pozdrowienia

+0

Gdzie jest tam ListView? Czy możesz dać nam o wiele więcej kodu, jak super dużo więcej. Daj nam wszystko, co masz. –

+0

Jeśli chcesz używać kart, dlaczego nie użyjesz kontrolki TabControl? Aby ukryć/pokazać zakładki, manipulujemy właściwością widoczności elementów sterujących TabItem (tutaj możesz użyć wiązania). Ponadto należy zapoznać się z przeglądem powiązania danych od firmy Microsoft http://msdn.microsoft.com/en-us/library/ms752347.aspx. Radziłbym nie wiązać elementów interfejsu użytkownika. W twoim przykładzie utworzę klasę dla strony SettingsPage, która będzie zawierała wiele właściwości dla ustawień. W Xaml utworzę formanty i powiązam każdą właściwość. – failedprogramming

+0

@ snowy gui hedgehog: ListView jako taki nie jest ważny, jest po to, aby wywołać zdarzenie changeitem, w którym mam zamiar ustawić zawartość ContentControl. Zasadniczo moje pytanie dotyczy jak dynamicznie zmieniać zawartość ContentControl z kodu za pomocą predefiniowanych szablonów ContentControl. @failedprogramming Powodem, dla którego próbuję to zrobić jest ten wpis: [link] (http://stackoverflow.com/questions/7010688/wpf-tab-control-with-no-tabs) tutaj. Dlaczego radzisz nie wiązać elementów interfejsu użytkownika? – Xaser

Odpowiedz

60

Ok mam ciążę prosty przykład, aby pokazać, jak można dynamicznie zmieniać zawartość ContentControl używając podejście MVVM (Model-View-ViewModel) z wiązania danych.

Zalecam utworzenie nowego projektu i załadowanie tych plików, aby zobaczyć, jak to wszystko działa.

Najpierw musimy zaimplementować interfejs INotifyPropertyChanged. Umożliwi to zdefiniowanie własnych klas z właściwościami, które będą powiadamiać interfejs użytkownika, gdy nastąpi zmiana właściwości. Tworzymy abstrakcyjną klasę, która zapewnia tę funkcjonalność.

ViewModelBase.cs

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

    protected void OnPropertyChanged(string propertyName) 
    { 
     this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); 
    } 

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) 
    { 
     var handler = this.PropertyChanged; 
     if (handler != null) 
     { 
      handler(this, e); 
     } 
    } 
} 

Teraz musimy mieć modeli danych. Dla uproszczenia stworzyłem 2 modele - HomePage i SettingsPage. Oba modele mają tylko jedną właściwość, możesz dodać więcej właściwości zgodnie z wymaganiami.

HomePage.cs

public class HomePage 
{ 
    public string PageTitle { get; set; } 
} 

SettingsPage.cs

public class SettingsPage 
{ 
    public string PageTitle { get; set; } 
} 

Następnie tworzą odpowiednie ViewModels owinąć każdego modelu. Zwróć uwagę, że viewmodels dziedziczą z mojej klasy abstrakcji ViewModelBase.

HomePageViewModel.cs

public class HomePageViewModel : ViewModelBase 
{ 
    public HomePageViewModel(HomePage model) 
    { 
     this.Model = model; 
    } 

    public HomePage Model { get; private set; } 

    public string PageTitle 
    { 
     get 
     { 
      return this.Model.PageTitle; 
     } 
     set 
     { 
      this.Model.PageTitle = value; 
      this.OnPropertyChanged("PageTitle"); 
     } 
    } 
} 

SettingsPageViewModel.cs

public class SettingsPageViewModel : ViewModelBase 
{ 
    public SettingsPageViewModel(SettingsPage model) 
    { 
     this.Model = model; 
    } 

    public SettingsPage Model { get; private set; } 

    public string PageTitle 
    { 
     get 
     { 
      return this.Model.PageTitle; 
     } 
     set 
     { 
      this.Model.PageTitle = value; 
      this.OnPropertyChanged("PageTitle"); 
     } 
    } 
} 

Teraz musimy zapewnić widoki dla każdego ViewModel. tj. HomePageView i SettingsPageView. Stworzyłem 2 UserControls do tego.

HomePageView.XAML

<UserControl x:Class="WpfApplication3.HomePageView" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     mc:Ignorable="d" 
     d:DesignHeight="300" d:DesignWidth="300"> 
<Grid> 
     <TextBlock FontSize="20" Text="{Binding Path=PageTitle}" /> 
</Grid> 

SettingsPageView.xaml

<UserControl x:Class="WpfApplication3.SettingsPageView" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     mc:Ignorable="d" 
     d:DesignHeight="300" d:DesignWidth="300"> 
<Grid> 
    <TextBlock FontSize="20" Text="{Binding Path=PageTitle}" /> 
</Grid> 

Teraz musimy określić XAML dla MainWindow. Dołączyłem 2 przyciski do nawigacji pomiędzy 2 stronami. MainWindow.xaml

<Window x:Class="WpfApplication3.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:WpfApplication3" 
    Title="MainWindow" Height="350" Width="525"> 
<Window.Resources> 
    <DataTemplate DataType="{x:Type local:HomePageViewModel}"> 
     <local:HomePageView /> 
    </DataTemplate> 
    <DataTemplate DataType="{x:Type local:SettingsPageViewModel}"> 
     <local:SettingsPageView /> 
    </DataTemplate> 
</Window.Resources> 
<DockPanel> 
    <StackPanel DockPanel.Dock="Left"> 
     <Button Content="Home Page" Command="{Binding Path=LoadHomePageCommand}" /> 
     <Button Content="Settings Page" Command="{Binding Path=LoadSettingsPageCommand}"/> 
    </StackPanel> 

    <ContentControl Content="{Binding Path=CurrentViewModel}"></ContentControl> 
</DockPanel> 

Musimy także ViewModel dla MainWindow. Ale wcześniej musimy stworzyć kolejną klasę, abyśmy mogli przypiąć nasze Przyciski do Poleceń.

DelegateCommand.cs

public class DelegateCommand : ICommand 
{ 
    /// <summary> 
    /// Action to be performed when this command is executed 
    /// </summary> 
    private Action<object> executionAction; 

    /// <summary> 
    /// Predicate to determine if the command is valid for execution 
    /// </summary> 
    private Predicate<object> canExecutePredicate; 

    /// <summary> 
    /// Initializes a new instance of the DelegateCommand class. 
    /// The command will always be valid for execution. 
    /// </summary> 
    /// <param name="execute">The delegate to call on execution</param> 
    public DelegateCommand(Action<object> execute) 
     : this(execute, null) 
    { 
    } 

    /// <summary> 
    /// Initializes a new instance of the DelegateCommand class. 
    /// </summary> 
    /// <param name="execute">The delegate to call on execution</param> 
    /// <param name="canExecute">The predicate to determine if command is valid for execution</param> 
    public DelegateCommand(Action<object> execute, Predicate<object> canExecute) 
    { 
     if (execute == null) 
     { 
      throw new ArgumentNullException("execute"); 
     } 

     this.executionAction = execute; 
     this.canExecutePredicate = canExecute; 
    } 

    /// <summary> 
    /// Raised when CanExecute is changed 
    /// </summary> 
    public event EventHandler CanExecuteChanged 
    { 
     add { CommandManager.RequerySuggested += value; } 
     remove { CommandManager.RequerySuggested -= value; } 
    } 

    /// <summary> 
    /// Executes the delegate backing this DelegateCommand 
    /// </summary> 
    /// <param name="parameter">parameter to pass to predicate</param> 
    /// <returns>True if command is valid for execution</returns> 
    public bool CanExecute(object parameter) 
    { 
     return this.canExecutePredicate == null ? true : this.canExecutePredicate(parameter); 
    } 

    /// <summary> 
    /// Executes the delegate backing this DelegateCommand 
    /// </summary> 
    /// <param name="parameter">parameter to pass to delegate</param> 
    /// <exception cref="InvalidOperationException">Thrown if CanExecute returns false</exception> 
    public void Execute(object parameter) 
    { 
     if (!this.CanExecute(parameter)) 
     { 
      throw new InvalidOperationException("The command is not valid for execution, check the CanExecute method before attempting to execute."); 
     } 
     this.executionAction(parameter); 
    } 
} 

A teraz możemy defind się MainWindowViewModel. CurrentViewModel jest właściwością związaną z ContentControl w MainWindow. Kiedy zmieniamy tę właściwość, klikając przyciski, ekran zmienia się w MainWindow. MainWindow wie, który ekran (usercontrol) załadować z powodu DataTemplates, które zdefiniowałem w sekcji Window.Resources.

MainWindowViewModel.cs

public class MainWindowViewModel : ViewModelBase 
{ 
    public MainWindowViewModel() 
    { 
     this.LoadHomePage(); 

     // Hook up Commands to associated methods 
     this.LoadHomePageCommand = new DelegateCommand(o => this.LoadHomePage()); 
     this.LoadSettingsPageCommand = new DelegateCommand(o => this.LoadSettingsPage()); 
    } 

    public ICommand LoadHomePageCommand { get; private set; } 
    public ICommand LoadSettingsPageCommand { get; private set; } 

    // ViewModel that is currently bound to the ContentControl 
    private ViewModelBase _currentViewModel; 

    public ViewModelBase CurrentViewModel 
    { 
     get { return _currentViewModel; } 
     set 
     { 
      _currentViewModel = value; 
      this.OnPropertyChanged("CurrentViewModel"); 
     } 
    } 

    private void LoadHomePage() 
    { 
     CurrentViewModel = new HomePageViewModel(
      new HomePage() { PageTitle = "This is the Home Page."}); 
    } 

    private void LoadSettingsPage() 
    { 
     CurrentViewModel = new SettingsPageViewModel(
      new SettingsPage(){PageTitle = "This is the Settings Page."}); 
    } 
} 

I wreszcie, musimy zastąpić uruchamiania aplikacji tak, że możemy załadować naszą klasę MainWindowViewModel do właściwości DataContext z MainWindow.

App.xaml.cs

public partial class App : Application 
{ 
    protected override void OnStartup(StartupEventArgs e) 
    { 
     base.OnStartup(e); 

     var window = new MainWindow() { DataContext = new MainWindowViewModel() }; 
     window.Show(); 
    } 
} 

Byłoby to także dobry pomysł, aby usunąć kod StartupUri="MainWindow.xaml" w tagu App.xaml aplikacji tak, aby nie dostać 2 MainWindows na rozruchu.

Należy pamiętać, że klasy DelegateCommand i ViewModelBase mogą być kopiowane do nowych projektów i używane. To tylko bardzo prosty przykład. Można uzyskać lepszy pomysł od here i here

Edycja w komentarzu, chciał wiedzieć, czy możliwe jest, aby nie mieć klasę dla każdego widoku i związanej standardowy kod. O ile mi wiadomo, odpowiedź brzmi: nie. Tak, możesz mieć pojedynczą gigantyczną klasę, ale nadal musisz wywołać OnPropertyChanged dla każdego Settera właściwości. Istnieje również kilka wad tego. Po pierwsze, powstała klasa byłaby naprawdę trudna do utrzymania. Byłoby dużo kodu i zależności. Po drugie, trudno byłoby użyć DataTemplates do "wymiany" widoków. Wciąż jest to możliwe dzięki użyciu klucza x: w DataTemplates i na twardym kodowaniu powiązania szablonu w kontrolerze użytkownika. W gruncie rzeczy nie powoduje to, że kod jest znacznie krótszy, ale będzie to dla ciebie trudniejsze.

Zgaduję, że twoim głównym zarzutem jest napisanie tak wielu kodów w swoim widoku, aby zawinąć właściwości twojego modelu.Spójrz na T4 templates. Niektórzy programiści używają tego do automatycznego generowania swojego kodu standardowego (tj. Klas ViewModel). Nie używam tego osobiście, używam niestandardowego fragmentu kodu, aby szybko wygenerować właściwość viewmodel.

Inną opcją byłoby użycie architektury MVVM, takiej jak Prism lub MVVMLight. Nie użyłem go samemu, ale słyszałem, że niektóre z nich mają wbudowane funkcje ułatwiające kodowanie.

Inną kwestią wartą zapamiętania jest: Jeśli przechowujesz swoje ustawienia w bazie danych, możliwe jest użycie struktury ORM, takiej jak Entity Framework, do generowania modeli z bazy danych, co oznacza, że ​​pozostało Ci tylko utworzenie viewmodels i widoki.

+2

Bez obaw. Zapoznaj się z powyższą zmianą dotyczącą pytania na temat nadmiernego kodu. – failedprogramming

+0

Wielkie dzięki, mam wszystko działa do tej pory :) – Xaser

+1

@ Xaser Wolałbym, jeśli zamieścisz swoje pytanie na StackOverflow. W ten sposób uzyskasz więcej pomocy. Możesz przesłać mi link do nowych pytań, a postaram się pomóc. Dzięki – failedprogramming

Powiązane problemy