ItemsControl DataTemplate when using an arbitrary UserControl collection - wpf

Context
I am working on a all-in-one virtual desktop for my users where most tasks require input through a modal dialog. Some super users can navigate through many kinds of configuration dialogs, which leads to serveral occurences of 2nd level dialogs (the first modal dialog invokes another one). In compliance with internal design orientations, each of those dialogs are placed on top of a semi-transparent grayed out background.
Trying to avoid writing the gray-background-panel over and over, I figured I could use an ItemsControl in order to stack my dialogs. My proof of concept worked wonders using a collection of String. So far so good.
The problem
Things get odd when using a collection of UserControl as the ItemsSource. WPF displays the actual UserControl instead of the ItemTemplate. It feels like the template is not even used when the collection's items are UIElements.
Xaml
<ItemsControl ItemsSource="{Binding SomeList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="#AA000000">
<Label Content="Does it work" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Code Behind
Public ReadOnly Property SomeOtherList As UserControl()
Get
Return New UserControl() {New MyControl}
End Get
End Property
Public ReadOnly Property SomeList As String()
Get
Return New String() {"One item"}
End Get
End Property
The actual question
Is there any way to specify a template when the ItemsSource items already possess one? Going further, can we even template a wrapper arround UserControl while leaving the actual UserControl untouched?
I know the whole thing could be cheated using code behind, but relying on VB or C# would not go through code review unnoticed. We're looking for an XAML solution here.
P.S. I am open to new solutions as long as there is a single, unified way to invoke an arbitrary number of dialogs.

After two days of tests I admited the initial approach was not possible, or at least to the extend of my knowledge. I came up with a decent wrapping solution.
Wrapper
<UserControl x:Class="HolderPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="#AA000000" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border Style="{StaticResource ResourceKey=borderBase}" />
<Grid Name="placeHolder" Margin="10" />
</Grid>
</UserControl>
The wrapper's code behind has a constructor to which you pass a UserControl to be placed in placeHolder's children, which lets you use it as shown below:
Usage
Private Shared _popup As ObservableCollection(Of UserControl)
Private Shared ReadOnly Property Popup As ObservableCollection(Of UserControl)
Get
If _popup Is Nothing Then _popup = New ObservableCollection(Of UserControl)
Return _popup
End Get
End Property
Public Shared Sub ModalPush(item As UserControl)
Popup.Add(New HolderPopup(item))
End Sub
Public Shared Sub ModalPop()
If Popup.Count > 0 Then Popup.RemoveAt(Popup.Count - 1)
End Sub
'For WPF binding
Public ReadOnly Property PopupRef As ObservableCollection(Of UserControl)
Get
Return Main.Popup()
End Get
End Property
Any event handler, anywhere in the application can call Main.ModalPush in order to stack a modal window on top of whathever was already there.
While this solution respects the constraints (unified popup handling without forcing some hackish dependency in my popups) I am not entirely satisfied. I feel it should have been possible through templating, which would have the benefit of removing this new wrapper class. All in all, that's an alternative, but not exactly what I was looking for.

Related

RibbonGallery.SelectedValue not updating SelectedItem

I'm using the Microsoft ribbon library: System.Windows.Controls.Ribbon.
I know this question looks big, but what I'm trying to do is actually not that complicated, there are just a few pieces involved.
The Goal
I'm trying to bind the selection of a RibbonComboBox to a property of one of my classes, which I'll call TestBindingSource, but I need to be able to cancel the change of selection. So if they select an item from the RibbonComboBox, but then cancel that change, the selection needs to stay as it was.
The items being show in the RibbonComboBox represent members of an Enum, which I'll call TestEnum. I built another class TestEnumGalleryItem to represent the TestEnum values in the RibbonComboBox and I use the RibbonGallery.SelectedValue and RibbonGallery.SelectedValuePath to bind to the property on TestBindingSource. In case that was too hard to follow, you should be able to see what I mean from my code.
The Code
The below is a simplified version of my real code, try not to dock me too many points for style. I've tested this in a new project and it can be used to show the problem I'm running into. Remember to add a reference to the Microsoft ribbon library.
MainWindow.xaml
<Window x:Class="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:VBTest"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Ribbon>
<RibbonTab Header="Test">
<RibbonGroup>
<RibbonComboBox Name="TestComboBox">
<RibbonGallery Name="TestGallery" MaxColumnCount="1" ScrollViewer.VerticalScrollBarVisibility="Auto" SelectedValuePath="EnumValue" SelectedValue="{Binding BindingSource.TestEnumValue}">
<RibbonGallery.ItemsSource>
<x:Array Type="local:TestEnumGalleryCategory">
<local:TestEnumGalleryCategory/>
</x:Array>
</RibbonGallery.ItemsSource>
<RibbonGallery.CategoryTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<RibbonGalleryItem ToolTipTitle="{Binding EnumName}" ToolTipDescription="{Binding EnumDescription}">
<TextBlock Text="{Binding EnumName}" Margin="0, -3, -0, -3"/>
</RibbonGalleryItem>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</RibbonGallery.CategoryTemplate>
</RibbonGallery>
</RibbonComboBox>
<RibbonButton Label="Break" Click="RibbonButton_Click"/>
</RibbonGroup>
</RibbonTab>
</Ribbon>
</Grid>
</Window>
MainWindow.xaml.vb
Imports System.ComponentModel
Class MainWindow
Public Sub New()
BindingSource = New TestBindingSource
InitializeComponent()
End Sub
Public Property BindingSource As TestBindingSource
Get
Return GetValue(BindingSourceProperty)
End Get
Set(ByVal value As TestBindingSource)
SetValue(BindingSourceProperty, value)
End Set
End Property
Public Shared ReadOnly BindingSourceProperty As DependencyProperty =
DependencyProperty.Register("BindingSource",
GetType(TestBindingSource), GetType(MainWindow))
Private Sub RibbonButton_Click(sender As Object, e As RoutedEventArgs)
Stop
End Sub
End Class
Public Enum TestEnum
ValueA
ValueB
End Enum
Public Class TestEnumGalleryCategory
Public Property Items As New List(Of TestEnumGalleryItem) From {New TestEnumGalleryItem With {.EnumValue = TestEnum.ValueA, .EnumName = "Value A", .EnumDescription = "A's description"},
New TestEnumGalleryItem With {.EnumValue = TestEnum.ValueB, .EnumName = "Value B", .EnumDescription = "B's description"}}
End Class
Public Class TestEnumGalleryItem
Public Property EnumValue As TestEnum = TestEnum.ValueA
Public Property EnumName As String
Public Property EnumDescription As String
End Class
Public Class TestBindingSource
Implements INotifyPropertyChanged
Private _TestEnumValue As TestEnum = TestEnum.ValueA
Property TestEnumValue As TestEnum
Get
Return _TestEnumValue
End Get
Set(value As TestEnum)
'Don't actually set new value, just leave it the same to simulate cancelation
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(TestEnumValue)))
End Set
End Property
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class
The Problem
You'll see when you run the code that the RibbonComboBox displays "Value A" by default. Change the selection to "Value B". The selection of the RibbonComboBox changes and now displays "Value B". This is not what I want to happen, the selection should immediately change back to "Value A".
If you look at the code for TestBindingSource.TestEnumValue, you'll see that I don't actually keep the new value when set, instead I leave the old one to simulate the user canceling the change. I then raise the PropertyChanged event to update the UI so it knows what the real value of the property is.
After you've changed to "Value B", click the "Break" button (included for convenience). In the Visual Studio watch window, compare the values of TestGallery.SelectedItem and TestGallery.SelectedValue. You'll see that TestGallery.SelectedValue holds the correct TestEnum value ValueA. Now look at TestGallery.SelectedItem and you'll see that it still holds the item representing ValueB.
So even though the RibbonGallery has been properly informed that the value should now be ValueA, it's still showing ValueB. How can I fix that?
I'll level with you, I don't have a ton of time to spend on this bug, and I'm used to having to make hacky workarounds when it comes to the ribbon. Any solution you can give me on how to get the
RibbonGallery (and hence the RibbonComboBox) to update correctly will be appreciated.
After more testing and research, I realized that this problem is not unique to the ribbon library. It actually seems to be a problem with the normal ComboBox too and presumably all ItemsControls. Once I realized that, I was able to search for an answer more effectively and found the solution here:
https://nathan.alner.net/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box/
It's not a perfectly clean solution, but in my specific case setting the value to the new selection and then immediately setting it back doesn't cause any problems, so that's what I did. For the record, when setting the selection back, I used DispatcherPriority.DataBind instead of DispatcherPriority.ContextIdle, this way the change never even shows in the UI but the solution still works.

ListBox Binding with Global Index

My application has a couple of ObservableCollections, one which is nested within an element of the other. Each contain a number of fields e.g.
ObservableCollectionA (called Listings)
Title
Description
Address
Images As MediaItems
ObservableCollectionB (called MediaItems)
ImageName
Src
DisplayTime
Currently I have been accessing ObservableCollections as follows:
Listings(0).MediaItems(0).ImageName
My application has the main Window display the items from Listings and a UserControl which contains a ListBox which displays the items from MediaItems.
Currently my Window is bound to Listings using code in the New method:
Dim AppLocal As Program = Application.CurrentItem
AppLocal.CurrentItem = 0
Me.DataContext = Listings.Item(AppLocal.CurrentItem)
For the Listings ObservableCollection, the UserControl has a XAML DataContext which references a local method which pulls the records from the nested MediaItems ObservableCollection.
<UserControl.DataContext>
<ObjectDataProvider ObjectType="{x:Type local:ThumbImageLoader}" MethodName="LoadImagesv2" IsAsynchronous="True" />
</UserControl.DataContext>
<Grid x:Name="ThumbListBoxGrid">
<ListBox x:Name="ThumbListBox" ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" />
</Grid>
The method is here:
Public NotInheritable Class ThumbImageLoader
Public Shared Function LoadImagesv2() As List(Of MediaItems)
Dim AppLocal As Program = Application.Current
Dim ThumbImages As New List(Of MediaItems)
ThumbImages = Listings(AppLocal.CurrentItem).MediaItems
Return ThumbImages
End Function
End Class
Whilst developing the UI layout I have just been binding the first item (0 index). I now want to be able to set AppLocal.CurrentItem from anywhere in the application so the Window and the ListBox are updated.
Ideally I would like it so when the global property index changes, the UI is updated.
How do I do this?
Is there a better way to go about it?
Ben
Ok, I discovered the joy of CollectionView. Offered exactly what I was after and was excrutiatingly easy to implement. I was blown away at not only how easy it was to implement, but I managed to cut out more lines of code than I used to implement it.
I implemented a public CollectionViewSource
Public ListingDataView As CollectionViewSource
In my main Window, I implemeted it as follows:
<CollectionViewSource x:Key="ListingDataView" />
and bound my top-level Grid to it:
<Grid DataContext="{Binding Source={StaticResource ListingDataView}}">
In my Application Startup I set the CollectionView Source
AppLocal.ListingDataView = CType(Application.Current.MainWindow.Resources("ListingDataView"), CollectionViewSource)
AppLocal.ListingDataView.Source = Listings
The next part which impressed me the most was implementing it for my User Control. I remembered the UserControl is inheriting from the main window so it has access to the CollectionView already, so I ditched the separate Class and Method binding in favour for this:
<ListBox ItemsSource="{Binding Path=MediaItems}" VerticalAlignment="Top" IsSynchronizedWithCurrentItem="True" />
Now whene I want to set the Current List Index, I simply call this:
AppLocal.ListingDataView.View.MoveCurrentToPosition(AppLocal.CurrentProperty)
A few milliseconds later, the UI updates automatically.
Done!!
When you want multiple source data (like your observable collection properties and the index for the observable collection) to a single target you should use MultiBinding.
So in your case somethign like this should help...
<ListBox x:Name="ThumbListBox" IsSynchronizedWithCurrentItem="True" >
<ListBox.ItemsSource>
<MultiBinding Converter="{StaticResource CollectionAndIndexCollaborator}">
<Binding Path="Listings" />
<Binding Path="Application.CurrentItem.CurrentItem" />
</MultiBinding>
</ListBox.ItemsSource>
</ListBox>
provided that ....
Your data context is some class that holds the Application object via a property of same name Application and the Listings collection via property of same name Listings.
Your DataContext class and Application class must have INotifyPropertyChanged implemented. It should also raise notifications for Application and Setter of CurrentItem and CurrentItem.CurrentItem properties.
CollectionAndIndexCollaborator.Convert() method returns the same indexed value as the final collection....
return ((ObservableCollection)values[0]).Item(int.Parse(values[1])) ;
where assuming MyListingType is the T of your Listings collection.
This way when anyone changes the global Application.CurrentItem.CurrentItem the multi binding above will get notified and will select the required Listings item.

Setting the objectinstance to the current data item

I am fairly new to WPF, have been working on finding an answer to this for a couple days without much luck, it seems like there should be a way. I have set up a DataTemplate whose DataType is a custom class of mine. Within the DataTemplate definition, I have set up a resources collection using . I did this because I want to create an ObjectDataProvider that will be available to the controls in the DataTemplate - I want the ObjectInstance of this ObjectDataProvider, to be currently bound data item (teh current instance within a list, of my custom class) - because then I want to be able to run a method on the current data instance - when the user changes the text in a textbox that is part of the DataTemplate. Hard to explain but this should make it clearer, here is my xaml:
<DataTemplate x:Key="TierDisplay" DataType="{x:Type tiers:PopulatedTier}">
<DataTemplate.Resources>
<ObjectDataProvider x:Key="FilteredItems" MethodName="GetDisplayItems">
<ObjectDataProvider.MethodParameters>
<sys:Int32>0</sys:Int32>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</DataTemplate.Resources>
<StackPanel Orientation="Vertical">
<TextBox Name="txtMaxSupplyDays" LostFocus="txtMaxSupplyDays_LostFocus"></TextBox>
<DataGrid ItemsSource="{Binding Source={StaticResource FilteredItems}}" />
</StackPanel>
</DataTemplate>
Each instance of the DataTemplate is bound to an instance of the PopulatedTier class. When the user leaves the textbox, txtMaxSupplyDays, I have code in the code-behind to take the value they have entered, and put it into the first MethodParameter of my ObjectDataProvider (whose key is FilteredItems). This works fine using the C# code-behind below, the code finds FilteredItems and plugs the desired value into the MethodParameter. But I can't figure how to tie FilteredItems into the current instance of PopulatedTier so that its GetDisplayItems will run. (If this worked, then presumably the DataGrid would refresh, using the output of GetDisplayItems as its ItemsSource.) In fact, in the C# below, it finds/recognizes the DataContext property of the textbox (sender) as being an instance of PopulatedTier. But how can I refer to this in the XAML within the ObjectDataProvider definition? THANK YOU and let me know if I can clarify further. Of cousre alternate suggestions are welcome; I'd like to keep as much in the XAML and out of the code-behind as I can.
private void txtMaxSupplyDays_LostFocus(object sender, RoutedEventArgs e)
{
var textBox = sender as TextBox;
if (textBox == null) return;
int value;
bool valueOK = Int32.TryParse(textBox.Text, out value);
if (valueOK)
((ObjectDataProvider)textBox.FindResource("FilteredItems")).MethodParameters[0] = value;
}
You have right thoughts about your code-behind - it have to be as small as possible. Its one of the slogan of MVVM pattern, that is what you need - learn MVVM. Internet have a lot of resources, so it wouldn't be a problem to find it.

WPF CAD-like system - all data in classes

Still picking my way through learning XAML/WPF. If someone can show me how to accomplish the following - it will go a long way to helping me develop my next project (and several similar projects down the road).
Say I have a collection of objects that defines objects to be drawn on a canvas. The objects contain all the information necessary to render the objects, including the shape, color, and location. Can a XAML control be created that binds to this collection and handles the rendering, or is this better done by drawing on the canvas in the code-behind?
One other point - the objects must eventually be click-selectable, selectable via rectangle-lasso, and draggable. This doesn't have to be solved in the example code someone supplies, but I thought it might be relevant to know this as it might affect the various implementations.
Example class below. thanks in advance.
Class DrawingElement
readonly property Shape as string ("circle", "square", "triangle")
readonly property Position as point (coordinates)
readonly property Color as string ("red", "blue", "yellow")
end class
Sub Main
dim lst as new List(of DrawingElement)
lst.add(new DrawingElement("Circle", 10,20, "Blue"))
lst.add(new DrawingElement("Square", 80,35, "Red"))
lst.add(new DrawingElement("Triangle", 210,120, "Yellow"))
<draw lst!>
End Sub
Can be done, but not by using magic strings (e.g., "circle") like in your example.
First, you should be designing your models based on existing framework elements rather than designing the model with the idea of whipping up some new UI elements or struggling to create code that interprets between them.
WPF already has an ellipse (circle), rectangle (square) and a whole host of other geometric primitives for you to use. You'll want to create models that contain public bindable properties that you can bind to instances of these elements to control their shape and location.
Without going into much detail (or testing), I'd do something like this
public class GeometricElement : INotifyPropertyChanged
{
// these are simplified and don't show INPC code
public double Left {get;set;}
public double Top {get;set;}
public Brush Fill {get;set;}
// ...
}
// ...
public class Square : GeometricElement
{
public double Width {get;set;}
public double Height {get;set;}
}
// ...
// bound to the window
public class CadAppDataContext
{
public ObservableCollection<GeometricElement> Elements {get; private set;}
}
And in my xaml, it would look something like
<ItemsControl ItemsSource="{Binding Source={StaticResource cadAppDataContext}}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Resources>
<DataTemplate TargetType="{x:Type me:Square}">
<Rectangle
Canvas.Left="{Binding Left}"
Canvas.Top="{Binding Top}"
Width="{Binding Width}"
Height="{Binding Height}"
Fill="{Binding Fill}" />
</DataTemplate>
<!-- more DataTemplates for circle, triangle, etc etc -->
</ItemsControl.Resources>
</ItemsControl>
The ItemsControl will create a Canvas element and for every GeometricElement in my Elements collection it will add a new child UI element based on the type of object in Elements.
Items controls are bound to collections and can add or remove elements based on what's happening in the collection within your code. It determines the UI element to add by looking for a DataTemplate that is designed for a particular Type. This is a common, and important, pattern and is why using magic strings will hurt you in the long run; the magic string route won't let you leverage the power of the framework already built into WPF.
Now, I'm not saying this will work out of the box. You'll probably run into properties that won't bind without some heavy lifting. You might even have to extend the geometry primitives to get them to behave how you want. But this is the pattern used in WPF applications. Understanding the pattern and using it will help you avoid hours of stress and failure.

How to do simple Binding in Silverlight?

I understand that Silverlight 3.0 has binding but just want a simple example on how to use this to read a property from a class.
I have a class called Appointment which as a String property called Location:
Public Property Location() As String
Get
Return _Location
End Get
Set(ByVal Value As String)
_Location = Value
End Set
End Property
With a Private Declaration for the _Location as String of course.
I want a XAML element to bind to this property to display this in a TextElement, but it must be in XAML and not code, for example I want something like this:
<TextBlock Text="{Binding Appointment.Location}"/>
What do I need to do to get this to work?
It has to be a Silverlight 3.0 solution as some WPF features are not present such as DynamicResource which is what I'm used to using.
Just to add that my XAML is being loaded in from a seperate XAML File, this may be a factor in why the binding examples don't seem to work, as there are different XAML files the same Appointment.Location data needs to be applied.
You have two options.
If the "Appointment" class can be used as the DataContext for the control or Window, you can do:
<TextBlock Text="{Binding Location}" />
If, however, "Appointment" is a property of your current DataContext, you need a more complex path for the binding:
<TextBlock Text="{Binding Path=Appointment.Location}" />
Full details are documented in MSDN under the Binding Declarations page. If neither of these are working, make sure you have the DataContext set correctly.
You need something in code, unless you want to declare an instance of Appointment in a resource and bind to that but I doubt thats what you want.
You need to bind the Text property to the Property Path "Location" then assign the DataContext of the containing XAML to an instance of the Appointment:-
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="{Binding Location}" />
</Grid>
Then in the control's load event:-
void Page_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Appointment() { Location = "SomePlace" };
}
Note in this case I'm using the default Page control.
If I'm reading correctly, you need to create an instance of Appointment, set the DataContext of the control to that instance and modify your binding to just say: Text="{Binding Location}"
Also, consider implementing INotifyPropertyChanged on your Appointment class to allow the data classes to notify the UI of property value changes.

Resources