Related
I have a function in my UI where I want to be able to collapse/make visible a text message depending on the value of a custom property in my window object.
Using online references, I have come up with this code-behind to register the property:
public bool ValidInterval
{
get { return pValidInterval; }
}
private bool pValidInterval = true;
public static readonly DependencyProperty ValidIntervalProperty = DependencyProperty.Register("ValidInterval", typeof(bool), typeof(Settings), new UIPropertyMetadata(true));
And this corresponding XAML for the label:
<Label Name="DynamicWarning" Content="Time interval must be a valid positive integer.">
<Label.Style>
<Style TargetType="Label">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ValidInterval}" Value="true">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding ValidInterval}" Value="false">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
Unfortunately, this does not work. I can get it to set the parameter depending on the initial value of the property, but doesn't update the visibility dynamically like I want. I have been looking at this for an hour and what I have seems consistent with examples I am finding online for similar operations. Am I doing something wrong, or can you not update the visibility on the fly? If the latter, how do I achieve an equivalent effect?
ΩmegaMan's answer is correct. But I wanted to clarify, you don't need a backing field with a dependency property, the static dependency property IS the backing field.
public bool ValidInterval
{
get { return (bool)GetValue(ValidIntervalProperty); }
set { SetValue(ValidIntervalProperty, value); }
}
However, if you aren't in need of a dependency object specifically, then you may just want to use INotifyPropertyChanged as ΩmegaMan has it right. Dependency properties are typically used when you need to bind another property to them, such as when making your own custom control. For example Visibility itself is the dependency property in your example and ValidInterval just needs to be a normal property that invokes the NotifyPropertyChanged event.
You need for the holder class of the properties which are bound to the page, to adhere to INotifyPropertyChanged Interface and implement it.
That process informs the bound controls on the page that something has changed, and when it has changed, then the control is "notified" of the change; then it reads afresh the property it is bound to.
For WPF/Xaml they specify the seperation of data concerns for the views to business logic, is done by implementing the Model-View-ViewModel or MVVM pattern.
The link provided is dry, and there are other resources which can describe on the net, but it simply says put all your business logic that is bound from the View to a separate View Model Class; which is instantiated on your View.
I provide a basic example, any version of .Net can be used, on my blog:
MVVM Example for Easier Binding
I cannot get item container from the ListBox in Backstage. Say, I have the following Backstage:
<!-- Backstage -->
<r:Ribbon.Menu>
<r:Backstage x:Name="backStage">
<r:BackstageTabControl>
<r:BackstageTabItem Header="Columns">
<Grid>
<ListBox Grid.Row="1" Grid.Column="0" x:Name="lstColumns"/>
</Grid>
</r:BackstageTabItem>
</r:BackstageTabControl>
</r:Backstage>
</r:Ribbon.Menu>
I fill it up:
public Root()
{
ContentRendered += delegate
{
var list = new List<int> { 1, 2, 3 };
foreach (var index in list)
{
lstColumns.Items.Add(index);
}
};
}
Next, I want to retrieve the item container (in this case - ListBoxItem) from the first entry of ListBox:
private void OnGetProperties(object sender, RoutedEventArgs e)
{
// Get first item container
var container = lstColumns.ItemContainerGenerator.ContainerFromIndex(0);
if (container is not null)
{
MessageBox.Show($"container = {container.GetType().FullName}");
}
else
{
MessageBox.Show("container is null");
}
}
But container is always null. But! If I open Backstage and then hide it, I see the message:
container = System.Windows.Controls.ListBoxItem.
So, I decided to add code which opens Backstage before filling it up:
backStage.IsOpen = true;
var list = new List<int> { 1, 2, 3 };
foreach (var index in list)
{
lstColumns.Items.Add(index);
}
backStage.IsOpen = false;
This works, but there's a flickering when you can barely see that Backstage is shown and hidden. This is not the perfect solution. So, how to get the item container?
P.S. Test project is here.
UPDATE (EXPLANATION)
The reason I need the item container is that I need to add set CheckBox state upon filling ListBox. This ListBoxis styled to contain CheckBoxes for items:
<Window.Resources>
<Style x:Key="CheckBoxListStyle" TargetType="ListBox">
<Setter Property="SelectionMode" Value="Multiple"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox Focusable="False"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter />
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
So, when I add text in the loop above, the CheckBoxgets created. I, then, need to set the states of those checkboxes, which come from JSON. So, I need something like this:
var list = new List<int> { 1, 2, 3 };
var json = JsonNode.Parse("""
{
"checked": true
}
""");
foreach (var index in list)
{
CheckBox checkBox = null;
var pos = lstColumns.Items.Add(index);
var container = lstColumns.ItemContainerGenerator.ContainerFromIndex(pos);
// Reach checkbox
// ...
// checkBox = ...
// ...
checkBox.IsChecked = json["checked"].GetValue<bool>();
}
And the problem is that container is always null.
Also, it doesn't matter whether I use Loaded or ContentRendered event - in either case container is null.
A High-Level Introduction
The reason that ContainerFromIndex returns null is that the container simply is not realized.
Returns the element corresponding to the item at the given index within the ItemCollection or returns null if the item is not realized.
This is controlled by the ItemContainerGenerator that is responsible for the following actions.
Maintains an association between the data view of a multiple-item control, such as ContainerFromElement and the corresponding UIElement tasks.
Generates UIElement items on behalf of a multiple-item control.
A ListBox is an ItemsControl that exposes the ItemsSource property for binding or assigning a collection.
A collection that is used to generate the content of the ItemsControl. The default is null.
Another option is to simply add items to the Items collection in XAML or code.
The collection that is used to generate the content of the ItemsControl. The default is an empty collection. [...]
The property to access the collection object itself is read-only, and the collection itself is read-write.
The Items property is of type ItemCollection, which is also a view.
If you have an ItemsControl, such as a ListBox that has content, you can use the Items property to access the ItemCollection, which is a view. Because it is a view, you can then use the view-related functionalities such as sorting, filtering, and grouping. Note that when ItemsSource is set, the view operations delegate to the view over the ItemsSource collection. Therefore, the ItemCollection supports sorting, filtering, and grouping only if the delegated view supported them.
You cannot use both ItemsSource and Items at the same time, they are related.
[...] you use either the Items or the ItemsSource property to specify the collection that should be used to generate the content of your ItemsControl. When the ItemsSource property is set, the Items collection is made read-only and fixed-size.
Both ItemsSource and Items either maintain a reference to or bind your data items, these are not the containers. The ItemContainerGenerator is responsible for creating the user interface elements or containers such as ListBoxItem and maintaining the relationship between the data and these items. These containers do not just exist throughout the lifecycle of your application, they get created and destroyed as needed. When does that happen? It depends. Containers are created or realized (using the internal terminology) when they are shown in the UI. That is why you only gain access to a container after it was first shown. How long they actually exist depends on factors like interaction, virtualization or container recycling. By interaction I mean any form of changing the viewport, which is the part of the list that you can actually see. Whenever items are scrolled into view, they need to be realized of course. For large lists with tens of thousands of items, realizing all containers in advance or keeping all containers once they are realized would hit performace and increase memory consumption drastically. That is where virtualization comes into play. See Displaying large data sets for reference.
UI Virtualization is an important aspect of list controls. UI virtualization should not be confused with data virtualization. UI virtualization stores only visible items in memory but in a data-binding scenario stores the entire data structure in memory. In contrast, data virtualization stores only the data items that are visible on the screen in memory.
By default, UI virtualization is enabled for the ListView and ListBox controls when their list items are bound to data.
This implies that containers are deleted, too. Additionally, there is container recycling:
When an ItemsControl that uses UI virtualization is populated, it creates an item container for each item that scrolls into view and destroys the item container for each item that scrolls out of view. Container recycling enables the control to reuse the existing item containers for different data items, so that item containers are not constantly created and destroyed as the user scrolls the ItemsControl. You can choose to enable item recycling by setting the VirtualizationMode attached property to Recycling.
The consequence of virtualization and container recycling is that containers for all items are not realized in general. There are only containers for a subset of your bound or assigned items and they may be recycled or detached. That is why it is dangerous to directly reference e.g. ListBoxItems. Even if virtualization is disabled, you can run into problems like yours, trying to access user interface elements with a different lifetime than your data items.
In essence, your approach can work, but I recommend a different approach that is much more stable and robust and compatible with all of the aforementioned caveats.
A Low-Level View
What is actually happening here? Let us explore the code in medium depth, as my wrists already hurt.
Here is the ContainerFromIndex method in the reference source of .NET.
The for loop in line 931 iterates ItemBlocks using the Next property of the _itemMap.
When your items were not shown, yet in the user interface, they are not realized.
In this case, Next will return an UnrealizedItemBlock (derivative of ItemBlock).
This item block will have a property ItemCount of zero.
The if condition in line 933 will not be met.
This continues until the item blocks are iterated and null is returned in line 954..
Once the ListBox and its items are shown, the Next iterator will return a RealizedItemBlock which has an ItemCount of greater than zero and will therefore yield an item.
How are the containers realized then? There are methods to generate containers.
DependencyObject IItemContainerGenerator.GenerateNext(), see line 230.
DependencyObject IItemContainerGenerator.GenerateNext(out bool isNewlyRealized), see line 239.
These are called in various places, like VirtualizingStackPanel - for virtualization.
protected internal override void BringIndexIntoView(int index), see line 1576, which does exactly what it is called. When an item with a certain index needs to be brought into view, e.g. through scrolling, the panel needs to create the item container in order to show the item in the user interface.
private void MeasureChild(...), see line 8005. This method is used when calculating the space needed to display a ListView, which is influenced by the number and size of its items as needed.
...
Over lots of indirections from a high-level ListBox over its base type ItemsControl, ultimately, the ItemContainerGenerator is called to realize items.
An MVVM Compliant Solution
For all the previously stated issues, there is a simple, yet superior solution. Separate your data and application logic from the user interface. This can be done using the MVVM design pattern. For an introduction, you can refer to the Patterns - WPF Apps With The Model-View-ViewModel Design Pattern article by Josh Smith.
In this solution I use the Microsoft.Toolkit.Mvvm NuGet package from Microsoft. You can find an introduction and a detailed documentation here. I use it because for MVVM in WPF you need some boilerplate code for observable objects and commands that would bloat the example for a beginner. It is a good library to start and later learn the details of how the tools work behind the scenes.
So let us get started. Install the aforementioned NuGet package in a new solution. Next, create a type that represents our data item. It only contains two properties, one for the index, which is read-only and one for the checked state that can be changed. Bindings only work with properties, that is why we use them instead of e.g. fields. The type derives from ObservableObject which implements the INotifyPropertyChanged interface. This interface needs to be implemented to be able to notify that property values changed, otherwise the bindings that are introduced later will not know when to update the value in the user interface. The ObservableObject base type already provides a SetProperty method that will take care of setting a new value to the backing field of a property and automatically notify its change.
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace RibbonBackstageFillTest
{
public class JsonItem : ObservableObject
{
private bool _isChecked;
public JsonItem(int index, bool isChecked)
{
Index = index;
IsChecked = isChecked;
}
// ...read-only property assumed here.
public int Index { get; }
public bool IsChecked
{
get => _isChecked;
set => SetProperty(ref _isChecked, value);
}
// ...other properties.
}
}
Now we implement a view model for your Root view, which holds the data for the user interface. It exposes an ObservableCollection<JsonItem> property that we use to store the JSON data items. This special collection automatically notifies if any items were added, removed or replaced. This is not necessary for your example, but you I guess it could be useful for you later. You can also replace the whole collection, as we again derived from ObservableObject and use SetProperty. The GetPropertiesCommand is a command, which is just an encapsulated action, an object that performs a task. It can be bound and replaces the Click handler later. The CreateItems method simply creates a list like in your example. The GetProperties is the method where you iterate the list and set your values from JSON. Adapt the code to your needs.
using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
namespace RibbonBackstageFillTest
{
public class RootViewModel : ObservableObject
{
private ObservableCollection<JsonItem> _jsonItems;
public RootViewModel()
{
JsonItems = CreateItems();
GetPropertiesCommand = new RelayCommand(GetProperties);
}
public ObservableCollection<JsonItem> JsonItems
{
get => _jsonItems;
set => SetProperty(ref _jsonItems, value);
}
public ICommand GetPropertiesCommand { get; }
private ObservableCollection<JsonItem> CreateItems()
{
return new ObservableCollection<JsonItem>
{
new JsonItem(1, false),
new JsonItem(2, true),
new JsonItem(3, false),
new JsonItem(4, true),
new JsonItem(5, false)
};
}
private void GetProperties()
{
foreach (var jsonItem in JsonItems)
{
jsonItem.IsChecked = // ...set your JSON values here.
}
}
}
}
The code-behind of your Root view is now reduced to its essentials, no data anymore.
using Fluent;
using Fluent.Localization.Languages;
using System.Threading;
using System.Windows;
namespace RibbonBackstageFillTest
{
public partial class Root
{
public Root()
{
InitializeComponent();
WindowStartupLocation = WindowStartupLocation.CenterScreen;
ContentRendered += delegate
{
if (Thread.CurrentThread.CurrentUICulture.Name != "en-US")
{
RibbonLocalization.Current.LocalizationMap.Clear();
RibbonLocalization.Current.Localization = new English();
}
};
}
}
}
At last, we create the XAML for the Root view. I have added comments for you to follow along. In essence, we add the new RootViewModel as DataContext and use data-binding to connect our data item collection with the ListBox via the ItemsSource property. Furthermore, we use a DataTemplate to define the appearance of the data in the user interface and bind the Button to a command.
<r:RibbonWindow x:Class="RibbonBackstageFillTest.Root"
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:r="urn:fluent-ribbon"
xmlns:local="clr-namespace:RibbonBackstageFillTest"
mc:Ignorable="d"
Title="Backstage Ribbon"
Height="450"
Width="800">
<r:RibbonWindow.DataContext>
<!-- This creates an instance of the root view model and assigns it as data context. -->
<local:RootViewModel/>
</Window.DataContext>
<Window.Resources>
<Style x:Key="CheckBoxListStyle"
TargetType="ListBox">
<Setter Property="SelectionMode" Value="Multiple" />
<!-- This is only used to style the containers, we do not need to change the control template -->
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="2" />
</Style>
</Setter.Value>
</Setter>
<!-- An item template is used to define the appearance of a data item. -->
<Setter Property="ItemTemplate">
<Setter.Value>
<!-- We create a data template for our custom item type. -->
<DataTemplate DataType="local:JsonItem">
<!-- The binding will loosely connect the IsChecked property of CheckBox with the IsChecked property of its JsonItem. -->
<!-- The binding is TwoWay by default, meaning that you can change IsChecked in code or in the UI by clicking the CheckBox. -->
<!-- The IsChecked value will always be synchronized in the view and view model. -->
<CheckBox Focusable="False"
IsChecked="{Binding Path=IsChecked}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</r:RibbonWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<r:Ribbon Grid.Row="0">
<!-- Backstage -->
<r:Ribbon.Menu>
<r:Backstage>
<r:BackstageTabControl>
<r:BackstageTabItem Header="Columns">
<Grid>
<!-- No need for a name anymore, we do not need to access controls. -->
<!-- The binding loosely connects the JsonItems collection with the ListBox. -->
<ListBox ItemsSource="{Binding JsonItems}"
Style="{StaticResource CheckBoxListStyle}"/>
</Grid>
</r:BackstageTabItem>
</r:BackstageTabControl>
</r:Backstage>
</r:Ribbon.Menu>
<!-- Tabs -->
<r:RibbonTabItem Header="Home">
<r:RibbonGroupBox Header="ID">
<!-- Instead of a Click event handler, we bind a command in the view model. -->
<r:Button Size="Large"
LargeIcon="pack://application:,,,/RibbonBackstageFillTest;component/img/PropertySheet.png"
Command="{Binding GetPropertiesCommand}"
Header="Properties"/>
</r:RibbonGroupBox>
</r:RibbonTabItem>
</r:Ribbon>
</Grid>
</r:RibbonWindow>
Now what is the difference? The data and your application logic is separated from the user interface. The data is always there in the view model, regardless of an item container. In fact, your data does not even know that there is a container or a ListBox. Whether the backstage is open or not, does not matter anymore, as you directly act on your data, not the user interface.
A Quicker And Dirtier Solution
I do not recommend this solution, it is just a quick and dirty solution apart from MVVM that might be easier to follow for you after you saw how to do it right. It uses the JsonItem type from before, but this time without an external library. Now you see what INotifyPropertyChanged does under the hood.
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace RibbonBackstageFillTest
{
public class JsonItem : INotifyPropertyChanged
{
private bool _isChecked;
public JsonItem(int index, bool isChecked)
{
Index = index;
IsChecked = isChecked;
}
// ...read-only property assumed here.
public int Index { get; }
public bool IsChecked
{
get => _isChecked;
set
{
if (_isChecked == value)
return;
_isChecked = value;
OnPropertyChanged();
}
}
// ...other properties.
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In your code-behind of the Root view, just create a field _jsonItems that stores the items. This field is used to access the list later in order to change the IsChecked values.
using Fluent;
using Fluent.Localization.Languages;
using System.Collections.Generic;
using System.Threading;
using System.Windows;
namespace RibbonBackstageFillTest
{
public partial class Root
{
private List<JsonItem> _jsonItems;
public Root()
{
InitializeComponent();
WindowStartupLocation = WindowStartupLocation.CenterScreen;
ContentRendered += delegate
{
if (Thread.CurrentThread.CurrentUICulture.Name != "en-US")
{
RibbonLocalization.Current.LocalizationMap.Clear();
RibbonLocalization.Current.Localization = new English();
}
};
_jsonItems = new List<JsonItem>
{
new JsonItem(1, false),
new JsonItem(2, true),
new JsonItem(3, false),
new JsonItem(4, true),
new JsonItem(5, false)
};
lstColumns.ItemsSource = _jsonItems;
}
private void OnGetProperties(object sender, RoutedEventArgs e)
{
foreach (var jsonItem in _jsonItems)
{
jsonItem.IsChecked = // ...set your JSON value.
}
}
}
}
At last for the Root view not much changes. We copy the style with the data template from the MVVM sample and set it to the ListBox. It will just behave the same, as your data is not dependent on view containers.
<r:RibbonWindow.Resources>
<Style x:Key="CheckBoxListStyle"
TargetType="ListBox">
<Setter Property="SelectionMode" Value="Multiple" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="2" />
</Style>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="local:JsonItem">
<CheckBox Focusable="False"
IsChecked="{Binding Path=IsChecked}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</r:RibbonWindow.Resources>
<ListBox x:Name="lstColumns"
Style="{StaticResource CheckBoxListStyle}"/>
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I'm creating multiple grids with textboxes and labels.
There are grids that have some textboxes in common and others ones are unique to each grid.
I'm using the visibility property to show and collapse each grid when I need it.
The question is, is there a way of making visible a textbox from a collapsed grid into a different visible grid while the grids are overlapping?
May be there is a better control to do this?
Here is a summarized example of what I want to do:
XAML
<ToolBarTray>
<ToolBar>
<Button Name="showgrid1" Click="showgrid1_Click"/>
<Button Name="showgrid2" Click="showgrid2_Click"/>
</ToolBar>
</ToolBarTray>
<Grid Name="grid1" Visibility="Collapsed">
<TextBox Name="Common"/>
<TextBox Name="UniqueTogrid1"/>
</Grid>
<Grid Name="grid2" Visibility="Collapsed">
<TextBox Name="UniqueTogrid2"/>
</Grid>
Code behind C#:
private void showgrid1_Click(object sender, RoutedEventArgs e)
{
grid1.Visibility = Visibility.Visible;
Common.Visibility = Visibility.Visible;
UniqueTogrid1.Visibility = Visibility.Visible;
grid2.Visibility = Visibility.Collapsed;
}
private void showgrid2_Click(object sender, RoutedEventArgs e)
{
grid1.Visibility = Visibility.Collapsed;
grid2.Visibility = Visibility.Visible;
UniqueTogrid2.Visibility = Visibility.Visible;
Common.Visibility = Visibility.Visible; //I want to show this textbox without declaring it in grid2 in XAML, while the grids are overlaping.
}
This code won't show the Common textbox in grid2.
Even with the edit, the question isn't entirely clear. In particular, your code example doesn't provide any actual values for the UI elements being displayed, rendering any change in visibility moot. Who cares what's visible when, if there's no data?
That said, from your use of the Click event as a way to respond to user input, I suspect that you similarly have some code somewhere that is explicitly setting the Text property for the named UI elements you're dealing with. This has led you to a desire to reuse the <TextBox Name="Common"/> element, so as to not duplicate code.
If that inference is correct, or even close to being correct, then…the motivation is honorable, but you've painted yourself into a corner through your improper use of the WPF API. Specifically, you should view the UI elements as throw-away objects, and keep all the interesting bits of your program in non-UI objects called "view models". See "MVVM" as a programming paradigm.
By "throw-away", I mean these objects are created as needed by the framework to serve the purposes of the current state of the UI. They should not take any more significant role than that. When I look at the code example you posted, there are at least a couple of major warning signs in the code: the elements have names, and there is code-behind that is manipulating their visual state.
Both of these characteristics are almost never needed in well-written WPF code. The XAML can very often completely describe not just the appearance of the UI but how it changes visual state based on the operation of the program.
Okay, so with that exposition out of the way, how to implement your code so that it better-suits the WPF API, while at the same time has minimal repetition?
I can see at least a couple of ways immediately. One is to preserve substantially the arrangement of the XAML as you've got it now, but move the important elements into a proper view model data structure. Another is to use data templates and multiple view model data structures to automatically update the UI according to what data is active at the moment. I'll show both approaches here.
Approach #1:
The first step is to create the view model. IMHO, a WPF program will almost always start with the view model, because the XAML (the user interface) exists to serve the view model (the program data), rather than the other way around. The view model should ideally not have any dependencies on the UI framework. It represents the state of your program independent of things specific to the framework, though it will often still have state that represents conditional aspects of the UI itself.
In some cases, you'll find that you choose to use the view model as an adapter between an even more-rigorous model data structure; this adds a new layer to the program, allowing for the model data structure to be entirely independent of UI concerns entirely. I didn't bother with that for this example.
class ViewModel1 : NotifyPropertyChangedBase
{
private string _commonText = "default common text view model 1";
public string CommonText
{
get => _commonText;
set => _UpdateField(ref _commonText, value);
}
private string _uniqueText1 = "default unique text #1";
public string UniqueText1
{
get => _uniqueText1;
set => _UpdateField(ref _uniqueText1, value);
}
private string _uniqueText2 = "default unique text #2";
public string UniqueText2
{
get => _uniqueText2;
set => _UpdateField(ref _uniqueText2, value);
}
private int _gridToShow;
public int GridToShow
{
get => _gridToShow;
set => _UpdateField(ref _gridToShow, value);
}
public ICommand SetGridToShowCommand { get; }
public ViewModel1()
{
SetGridToShowCommand = new SetGridToShow(this);
}
private class SetGridToShow : ICommand
{
private readonly ViewModel1 _owner;
public SetGridToShow(ViewModel1 owner)
{
_owner = owner;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter)
{
if (parameter is int index ||
(parameter is string text && int.TryParse(text, out index)))
{
_owner.GridToShow = index;
}
}
}
}
This class has a few features typical of WPF view models:
It inherits a base class that does the actual work of implementing INotifyPropertyChanged.
It has public properties that represent the current state of the program, and which will be used in bindings declared in the XAML, either to display a particular value or to control some particular state of the UI.
It has public properties (well, one in this case) for commands to react to user input. In this particular example, the implementation of the single command is a standalone nested class, but in a real-world program this would typically be generalized as well, using helper classes that do things like handling type conversion for command parameters and accepting delegates for the actual implementation of a command.
In this example, the view model includes three string properties, one that represents the shared value between the two UI states, and then two more, each of which being the "unique" value for each state, an int property that represents the current UI state, and an ICommand property that handles the user input.
With that view model declared, now we can look at the XAML:
<DockPanel>
<DockPanel.DataContext>
<l:ViewModel1/>
</DockPanel.DataContext>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="Show Grid 1" Command="{Binding SetGridToShowCommand}" CommandParameter="1"/>
<Button Content="Show Grid 2" Command="{Binding SetGridToShowCommand}" CommandParameter="2"/>
</ToolBar>
</ToolBarTray>
<Grid>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding GridToShow}" Value="1">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBox Text="{Binding CommonText}"/>
<TextBox Text="{Binding UniqueText1}"/>
</StackPanel>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding GridToShow}" Value="2">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBox Text="{Binding CommonText}"/>
<TextBox Text="{Binding UniqueText2}"/>
</StackPanel>
</Grid>
</DockPanel>
The important parts in the above, relative to your question, are:
Most important, the CommonText property is bound to two different TextBox elements. I.e. the XAML element is not shared (which would have been the literal answer to your question), but rather the underlying view model property is shared. This allows the UI to interact with the user in whatever manner is appropriate for the given UI state, while only have a single state in the view model that represents the user's input.
The view model object is set as the data context for this part of the visual tree, via the DockPanel.DataContext element binding.
The user input isn't implemented with handlers for the Click event, but rather via the ICommand that updates the view model state according to the input.
The UI state itself responds to the changes in the view model via DataTrigger elements provided in the Style elements set for each "grid" (I used a StackPanel instead of a Grid in this example, because it was more convenient, but the same general ideas apply regardless.)
Approach #2:
That example alone I think sufficiently addresses the scenario you describe. However, WPF also can display an entirely different configuration of UI elements for a given data context object, through the mechanism of data templates. If we apply that idea to your question, we can:
Establish a couple more view model objects to represent the "unique" values in the program.
Declare a template for each of the view model objects.
Instead of using DataTrigger to change the visual state of the UI, let WPF automatically update the state via the templates, by simply updating the current view model being displayed.
In this scheme, here are the view model objects I came up with…
The main one:
class ViewModel2 : NotifyPropertyChangedBase
{
private readonly ViewModel2A _viewModel2A = new ViewModel2A();
private readonly ViewModel2B _viewModel2B = new ViewModel2B();
public string CommonText => "common text view model 2";
private object _gridViewModel;
public object GridViewModel
{
get => _gridViewModel;
set => _UpdateField(ref _gridViewModel, value);
}
public ICommand SetGridToShowCommand { get; }
public ViewModel2()
{
SetGridToShowCommand = new SetGridToShow(this);
}
private class SetGridToShow : ICommand
{
private readonly ViewModel2 _owner;
public SetGridToShow(ViewModel2 owner)
{
_owner = owner;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter)
{
if (parameter is int index ||
(parameter is string text && int.TryParse(text, out index)))
{
_owner.SetGridToShowIndex(index);
}
}
}
private void SetGridToShowIndex(int index)
{
GridViewModel = index == 1 ? (object)_viewModel2A : _viewModel2B;
}
}
And the two "unique" ones:
class ViewModel2A
{
public string UniqueText1 => "unique text grid #1";
}
class ViewModel2B
{
public string UniqueText2 => "unique text grid #2";
}
I skipped the INotifyPropertyChanged for the purposes of this example, and just made these view models with read-only/display-only properties.
Note that in the main view model, all it does when the user input occurs, is to set the current "grid" view model to the appropriate "unique" view model object.
With that in place, we can write the XAML:
<DockPanel Grid.Column="1">
<DockPanel.DataContext>
<l:ViewModel2/>
</DockPanel.DataContext>
<DockPanel.Resources>
<DataTemplate DataType="{x:Type l:ViewModel2A}">
<StackPanel>
<!-- OneWay binding for illustration purposes (view model property is read-only) -->
<!-- RelativeSource allows for referencing properties from other than the current data context, such as the common text property -->
<TextBox Text="{Binding DataContext.CommonText, Mode=OneWay, RelativeSource={RelativeSource AncestorType=DockPanel}}"/>
<TextBox Text="{Binding UniqueText1, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type l:ViewModel2B}">
<StackPanel>
<TextBox Text="{Binding DataContext.CommonText, Mode=OneWay, RelativeSource={RelativeSource AncestorType=DockPanel}}"/>
<TextBox Text="{Binding UniqueText2, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</DockPanel.Resources>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="Show Grid 1" Command="{Binding SetGridToShowCommand}" CommandParameter="1"/>
<Button Content="Show Grid 2" Command="{Binding SetGridToShowCommand}" CommandParameter="2"/>
</ToolBar>
</ToolBarTray>
<Grid>
<ContentControl Content="{Binding GridViewModel}"/>
</Grid>
</DockPanel>
Here, rather than setting styles with triggers, there are two different templates declared in the resource dictionary of the parent DockPanel element, one for each "unique" view model type. Then in the Grid control, the content is simply bound to the current "unique" view model object. WPF will select the correct template according to the type of that current "unique" view model object.
One slightly complicated thing I did in the XAML above was to put the CommonText property in the main view model, making it actually common to both view states. Then the templates both refer to it by using the RelativeSource mode for the binding. It would also have been possible instead to have the data templates only provide UI elements for the "unique" properties, and have the parent UI element handle display of the CommonText property. That would arguably be a little cleaner and less repetitive, but it would also have been a significant enough departure from the code you originally posted that I decided not to cross that bridge. :)
Finally, all of the above relies on that base class I mentioned earlier to implement INotifyPropertyChanged. There are various ways to implement that, but to complete the above example, here's the implementation I used for the code above:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I have a UserControl that needs to be bind with two DataContext on the basis of a checkbox.
This UserControl have to display the data about the application (global) or selected DataGridRow.
class Person
{
public string Name {get; set;}
public string Age {get; set;}
}
UserControl has only two text fields to display name and Age. If "Global" checkbox is checked, then I want to bind this usercontrol with the property of APerson (of Person class) in MainViewModel and if it is unchecked then I have to bind the UserControl with the SelectedItem in DataGrid. SelectedItem is also a Person type
Basically you could just play with your ViewModel to get what you want. Here is one way to do it.
You would be binding the Checkbox.IsChecked to 'IsGlobal' property in the ViewModel.
Then you would be binding the userControl to another property, say SomePerson in the ViewModel.
Lastly, in the setter of IsGlobal you would change the SomePerson to either APerson or SelectedItem in your datagrid depending on the boolean state of IsGlobal.
[Adding this here since you want a way to do it purely in XAML. I think that insisting on pure XAML here is not essential and #bit's answer is the right way to go, IMO.]
You can use style to have triggers that do the change.
Let's say your UC is called MyUC and currently you have an instance of it similar to: <local:MyUC/> in some other view/UC/window. You can change the instance to look like so:
<local:MyUC>
<local:MyUC.Style>
<Style TargetType="{x:Type local:MyUC}">
<Setter Property="DataContext" Value="{Binding SelectedItem, ElementName=MyDataGrid}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=MyCheckbox, Path=IsChecked}"
Value="True">
<Setter Property="DataContext" Value="{Binding APerson}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</local:MyUC.Style>
</local:MyUC>
I'm changing here the data context property, but you can change any other dependency property on MyUC.
Again, I think this is a less favorite approach to tackle this functionality, but it's pure XAML.
please help me understand MVVM better:
I had been binding listboxes to lists of custom objects, but then I ran into some trouble with having selected items populate / bind correctly in a multi select list.
The solution I found used a List just for that list box, (or at least that's what I understood) and then the viewmodel class could have an IsSelected property, which worked for me.
My questions are this: Is this common to bind a listbox to a list of view models? it seems so strange, and if ti's normal, is binding a list box to a list of business objects bad MVVM technique?
Finally, when I bind a listbox to this list of viewmodels, how typically are those linked back to the list of business objects that they represent? do I keep an id in the view model, or a reference to the business object itself? Sorry for these questions, but I am trying to learn MVVM and do it right.
here is the viewmodel I am putting in a list and binding to the listbox:
class ItemViewModel
{
public ItemViewModel(string name)
{
Name = name;
}
public string Name { get; private set; }
bool isSelected;
public bool IsSelected
{
get {
return isSelected;
}
set {
isSelected = value;
}
}
}
here is the xaml for the listbox in the view
<ListBox Height="401" ItemsSource="{Binding Path=Users}" ItemTemplate="{StaticResource listBoxTemplate}" SelectionMode="Multiple" HorizontalAlignment="Left" Margin="202,29,0,0" Name="lbxAuthorizedUsers" VerticalAlignment="Top" Width="154" >
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>`
and here is the Users property in the view model
public List<ItemViewModel> Users
{
get { return this.users; }
set { this.users = value; }
}
What you have seems fine
The idea behind the MVVM design pattern is to keep your UI layer separate from the business logic layer. An ideal MVVM application can run with any UI (or no UI at all), so if you have some kind of business logic based off Selected Users, then you should either have a SelectedUsers collection on your ViewModel, or an IsSelected property on your User object.
As a side note, if you want WPF to automatically update it's UI when a property in your ViewModels or Models change, make sure they implement INotifyPropertyChanged. And if you want them to automatically update when a collection changes, make sure to use an ObservableCollection instead of a List
I started learning MVVM with this article by Josh Smith, which is a great introduction to the design pattern although when I was first starting out I had a hard time understanding it. If you're looking for something simpler, you can check out the simple MVVM example I wrote
Also, your ViewModel should implement INotifyPropertyChanged so that it can notify the View of any changes.