c# - Contenido ContentControl vinculante para contenido dinámico
wpf xaml (1)
Actualmente estoy tratando de lograr la funcionalidad de un tabcontrol con pestañas ocultas usando un ListView (como pestañas) y un ContentControl con Vinculando la propiedad del contenido.
Leí un poco sobre ese tema y si lo hice bien, debería funcionar de esta manera:
<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>
Y en el código detrás:
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;
Aunque no arroja ningún error, no muestra el TextBlock "Test".
Es probable que tenga el concepto de enlace incorrecto, por favor dame una pista en la dirección correcta.
Saludos
De acuerdo, puse un ejemplo simple para mostrarle cómo puede cambiar dinámicamente el contenido de ContentControl utilizando un enfoque MVVM (Model-View-ViewModel) con enlace de datos.
Le recomendaría que cree un nuevo proyecto y cargue estos archivos para ver cómo funciona todo.
Primero tenemos que implementar la interfaz INotifyPropertyChanged. Esto le permitirá definir sus propias clases con propiedades que notificarán a la UI cuando ocurra un cambio en las propiedades. Creamos una clase abstracta que proporciona esta funcionalidad.
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);
}
}
}
Ahora necesitamos tener los modelos de datos. Para simplificar, creé 2 modelos: HomePage y SettingsPage. Ambos modelos solo tienen una sola propiedad, puede agregar más propiedades según sea necesario.
HomePage.cs
public class HomePage
{
public string PageTitle { get; set; }
}
SettingsPage.cs
public class SettingsPage
{
public string PageTitle { get; set; }
}
Luego creo ViewModels correspondientes para ajustar cada modelo. Tenga en cuenta que los viewmodels heredan de mi clase abstracta 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");
}
}
}
Ahora necesitamos proporcionar Vistas para cada ViewModel. es decir, The HomePageView y SettingsPageView. Creé 2 UserControls para esto.
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>
Ahora necesitamos definir el xaml para MainWindow. He incluido 2 botones para ayudar a navegar entre las 2 "páginas". 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>
También necesitamos un ViewModel para MainWindow. Pero antes de eso necesitamos crear otra clase para que podamos vincular nuestros botones a los comandos.
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);
}
}
Y ahora podemos definir MainWindowViewModel. CurrentViewModel es la propiedad que está vinculada a ContentControl en MainWindow. Cuando cambiamos esta propiedad haciendo clic en los botones, la pantalla cambia en MainWindow. MainWindow sabe qué pantalla (usercontrol) cargar debido a los DataTemplates que he definido en la sección 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."});
}
}
Y, por último, tenemos que anular el inicio de la aplicación para que podamos cargar nuestra clase MainWindowViewModel en la propiedad DataContext de 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();
}
}
También sería una buena idea eliminar el StartupUri="MainWindow.xaml"
en la etiqueta de aplicación App.xaml para que no tengamos 2 MainWindows en el inicio.
Tenga en cuenta que las clases DelegateCommand y ViewModelBase solo pueden copiarse en proyectos nuevos y utilizarse. Este es solo un ejemplo muy simple. Puedes obtener una mejor idea desde here y here
Editar En su comentario, quería saber si es posible no tener que tener una clase para cada vista y el código repetitivo relacionado. Hasta donde yo sé, la respuesta es no. Sí, puede tener una única clase gigantesca, pero igual deberá llamar a OnPropertyChanged para cada Property setter. También hay bastantes inconvenientes para esto. En primer lugar, la clase resultante sería muy difícil de mantener. Habría un montón de código y dependencias. En segundo lugar, sería difícil usar DataTemplates para "intercambiar" vistas. Todavía es posible mediante el uso de ax: Teclee sus plantillas de datos y codifique una unión de plantilla en su usercontrol. Básicamente, no estás haciendo tu código mucho más corto, pero lo harás más difícil para ti.
Supongo que su principal queja es tener que escribir tanto código en su modelo de vista para ajustar las propiedades de su modelo. Echa un vistazo a las plantillas T4 . Algunos desarrolladores usan esto para autogenerar su código repetitivo (es decir, las clases de ViewModel). No lo uso personalmente, utilizo un fragmento de código personalizado para generar rápidamente una propiedad de modelo de vista.
Otra opción sería usar un marco de MVVM, como Prism o MVVMLight. No he usado uno yo mismo, pero he escuchado que algunos de ellos tienen funciones integradas para hacer que el código repetitivo sea fácil.
Otro punto a tener en cuenta es: si está almacenando su configuración en una base de datos, podría ser posible usar un marco ORM como Entity Framework para generar sus modelos desde la base de datos, lo que significa que todo lo que queda es crear los viewmodels y views.