Create Silverlight controls dynamically for each ItemsControl item - c#

I want to bind list of objects each of it has UIType (ie textbox, checkbox, combo etc). And it has object Value property.
I want to bind this list to an ItemControl (ListBox, DataGrid..) where each item will have separate template corresponding to the particular UIType of each object (eg item for combo will have combo in the row and item for checkbox will have checkbox)...
obviously prop Value will be bound to relevant property on each item.
whats the most transparent and not too elaborate way to achieve this?
Silverlight 5.
EDIT: (working code based on the Jacob's solution)
code:
ObservableCollection<UIType> data;
public MainPage()
{
InitializeComponent();
data = new ObservableCollection<UIType>()
{
new UITypeTextBox() { Value = "Value.." },
new UITypeCheckBox(){ Value = true },
};
lb.ItemsSource = data;
}
public class UIType { }
public class UITypeTextBox : UIType { public object Value { get; set; }}
public class UITypeCheckBox : UIType { public object Value { get; set; } }
xaml:
<ListBox x:Name="lb">
<ListBox.Resources>
<DataTemplate DataType="local:UITypeTextBox">
<TextBox Text="{Binding Value}" />
</DataTemplate>
<DataTemplate DataType="local:UITypeCheckBox">
<CheckBox IsChecked="{Binding Value}" />
</DataTemplate>
</ListBox.Resources>
</ListBox>

I am not sure about Silverlight but in WPF you can use data templates to do this. For each of your UI types you define a data template which basically maps a type to a view which is just a user control defined i XAML.
Typically you will define the data templates in a resource dictionary:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:MyApp.Views"
xmlns:uitypes="clr-namespace:MyApp.UITypes"
>
<DataTemplate DataType="{x:Type uitypes:TextBox}">
<views:TextBoxView />
</DataTemplate>
<DataTemplate DataType="{x:Type uitypes:CheckBox}">
<views:CheckBoxView />
</DataTemplate>
</ResourceDictionary>
Your views would be XAML files inheriting from UserControl. For example the XAML for the TextBox view might look like the following.
<UserControl x:Class="MyApp.Views.TextBox"
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"
<Grid>
<TextBox Text="{Binding Value}" />
</Grid>
</UserControl>
WPF (and hopefully Silverlight) automatically pics the right view when you add your UI types to an ItemControl.

Related

How to add a ListViewItem which contains a Grid, StackPanel and a TextBlock

I have a ListView. I need to add ListViewItems programatically that contain a Textblock nested inside of a StackPanel, nested inside of a Grid (For the purpose of formatting the text). I am relatively new to WPF and I cannot find an answer. Here is the code that I would like each ListViewItem to have once added:
<ListViewItem Padding="15">
<Grid Width="1285">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Width="Auto">
<TextBlock Text="ITEM" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</ListViewItem>
Here is an image to demonstrate what I am trying to do.The code above puts the ListViewItem in the middle, but by using a Grid and a StackPanel, I was able to center the text (StackPanel was actually for the purpose of adding an icon alongside it but I've temporarily taken that out. If someone knows how to do this better then by all means tell me.
So, what you need is a UserControl, which will be used to display each item in your ListView. So you must design your user control the way you want it to look; so if you need a TextBlock inside a panel inside a grid, that's what you must do.
<UserControl x:Class="SOWPF.MyListViewItem"
....
mc:Ignorable="d"
d:DesignHeight="48" d:DesignWidth="250">
<Grid>
<StackPanel Orientation="Horizontal" Width="250" Height="36" Margin="10" Background="PeachPuff">
<TextBlock Background="White" Width="200" Height="32" Margin="2" Text="{Binding DisplayText}"/>
</StackPanel>
</Grid>
</UserControl>
To display data, you must have a class with public properties. So I have this simple class with one public string property, which will contain the text you want to display in the TextBlock. The data binding on the user control refers to this; DisplayText is the public string property:
public class DisplayData
{
public string DisplayText { get; set; }
}
Now in your View, you must use a ContentControl inside your ListView to display your UserControl dynamically.
<Window x:Class="SOWPF.MainWindow"
....
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ListView>
<ItemsControl ItemsSource="{Binding DisplayList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MyListViewItem/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ListView>
</Grid>
</Window>
And here's your code behind. I did it this way for convenience, but you really should use a ViewModel.
public partial class MainWindow : Window
{
public List<DisplayData> DisplayList { get; set; }
public MainWindow()
{
InitializeComponent();
DisplayList = new List<DisplayData>
{
new DisplayData() { DisplayText = "A" },
new DisplayData() { DisplayText = "B" },
new DisplayData() { DisplayText = "C" }
};
DataContext = this;
}
}
Result:
EDIT (After OP edited the question)
If all you want to do is center text, you can get rid of extra controls and simply use TextAlignment=Center in your TextBlock.
<UserControl x:Class="SOWPF.MyListViewItem"
....
mc:Ignorable="d"
d:DesignHeight="48" d:DesignWidth="250">
<TextBlock Background="LightCoral" Width="200" Height="32" Margin="2" Text="{Binding DisplayText}"
TextAlignment="Center"/>
</UserControl>
And it'll look like this:

wpf bind different datatemplates to different types of objects in contentcontrol

I am new to WPF and MVVM. What I am trying to do is to bind two different DataTemplates to two different kinds of objects in one ContentControl. Each kind of object corresponds to one DataTemplate.
The two kinds of objects are called Unit and Component respectively. They contain different properties. For example a Unit has 3 properties: Id, Name and Manufacture. A Component has 3 properties Id, Type and Materials. The example code is as below:
public class Unit : INotifyPropertyChanged
{
private int _id;
private string _name;
private string _manufacture;
public int Id
{
get {return this._id}
set
{
this._id = value;
OnPropertyChanged("Id")
}
{
public string Name
{
get {return this._name}
set
{
this._id = value;
OnPropertyChanged("Name")
}
{
public string Manufacture
{
get {return this._manufacture}
set
{
this._id = value;
OnPropertyChanged("Manufacture")
}
{
public event PropertyChangedEventHandler PropertyChanged;
...
}
The Component class has the similar structure.
In the MainWindow, I have a ListBox listing names of objects (I will change it to a TreeView in the future) on the left, and a ContentControl on the right. I want that when I select the name of an object, the details of the object will be shown on the right. The code of the MainWindow is as below:
<Windows.Resources>
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=UnitItems}"
x:Key="UnitDataView">
</CollectionViewSource>
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=ComponentItems}"
x:Key="ComponentDataView">
</CollectionViewSource>
<CompositeCollection x:Key="AllDataView
<CollectionContainer Collection="{Binding Source={StaticResource UnitDataView}}" />
<CollectionContainer Collection="{Binding Source={StaticResource ComponentDataView}}" />
</CompositeCollection>
<local: PartDataTemplateSelector x:Key="MyDataTemplateSelector"
UnitTemplate="{StaticResource unitTemplate}"
ComponentTemplate="{StaticResource componentTemplate}" />
</Windows.Resources>
<Grid>
<Grid.ColumnDefinition>
<ColumnDefinition>
<ColumnDefinition>
</Grid.ColumnDefinition>
<ListBox x:Name="ComponentListView" Grid.Column="0"
ItemsSource="{Binding Source={StaticResource AllDataView}}" />
<TabControl Grid.Column="1"
<TabItem Header="Basic Info">
<ContentControl x:Name="BasicInfoContent"
ContentTemplateSelector="{StaticResource MyDataTemplateSelector}"
Content="{Binding Source={StaticResource AllDataView}}">
</ContentControl>
</TabItem>
</TabControl>
</Grid>
The UnitItems and ComponentItems are two ObservableCollection<T> objects defined in App.xaml.cs. And I have defined some DataTemplates in App.xaml. The example code is as below:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="..."
</ResourceDictionary.MergedDictionaries>
<DataTemplate DataType="{x:Type src:Unit}">
<!-- This template is to show the name of a unit object in the ListBox -->
</DataTemplate>
<DataTemplate DataType="{x:Type src:Component}">
<!-- This template is to show the name of a component object in the ListBox -->
</DataTemplate>
<DataTemplate x:Key="unitTemplate" DataType="{x:Type src:Unit}">
<!-- This template is to show the details of a unit object in the ContentControl -->
</DataTemplate>
<DataTemplate x:Key="componentTemplate" DataType="{x:Type src:Component}">
<!-- This template is to show the details of a component object in the ContentControl -->
</DataTemplate>
</Application.Resources>
And my custom DataTemplateSelector is as below:
class MyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate UnitTemplate { get; set; }
public DataTemplate ComponentTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
swith (item)
{
case Unit _:
return UnitTemplate;
case Component _:
return ComponentTemplate;
}
return null;
}
}
I have read this article ContentTemplateSelector and tried the ContentTemplateSelector, but since I use a CompositeCollection and CollectionContainer to bind these two kinds of objects in the ContentControl, the item object in my DataTemplateSelector class receives the CompositeCollection type, not a Unit type nor a Component type, so there is no proper template being returned. Also I tried the method mentioned in this article DataType Property, which is to set a DataType property for each of the DataTemplate and set the Path to "/". Maybe I misunderstood it, but it did not work either, where I think it has the same issue with the ContentTemplateSelector one. So anybody can help me on this problem?
It is my very first time to ask a question on Stack Overflow. I know some of my description and codes are trivial to this question, but I just don't want to miss any details that may be related to my problem. I apologise for that. Also if there are any problem with my coding style and data structure, please feel free to point it out. I really appreciate it. Thank you for your reading and help!
You do not need a DataTemplateSelector. Just make sure that the detail DataTemplates can be automatically selected, by not assining a key to them.
It also seems that you don't need two collections for your objects. You might as well derive both Unit and Component from a common base class and have a single collection of base class references.
Finally there should be a view model, which besides the objects collection also has a property for the currently selected object.
Take this simplified example view model:
public class Base
{
public int Id { get; set; }
}
public class Unit : Base
{
public string UnitData { get; set; }
}
public class Component : Base
{
public string ComponentData { get; set; }
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Base> Objects { get; }
= new ObservableCollection<Base>();
private Base selectedObject;
public Base SelectedObject
{
get { return selectedObject; }
set
{
selectedObject = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(SelectedObject)));
}
}
}
An instance of it should be assigned to the window's DataContext:
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
vm.Objects.Add(new Unit { Id = 1, UnitData = "Unit Data" });
vm.Objects.Add(new Component { Id = 2, ComponentData = "Component Data" });
DataContext = vm;
}
Finally, the XAML would be this:
<ListBox ItemsSource="{Binding Objects}"
SelectedItem="{Binding SelectedObject}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type local:Unit}">
<TextBlock>
<Run Text="Unit, Id:"/>
<Run Text="{Binding Id}"/>
</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Component}">
<TextBlock>
<Run Text="Component, Id:"/>
<Run Text="{Binding Id}"/>
</TextBlock>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<ContentControl Grid.Column="1" Content="{Binding SelectedObject}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:Unit}">
<StackPanel>
<TextBlock Text="{Binding Id}"/>
<TextBlock Text="{Binding UnitData}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Component}">
<StackPanel>
<TextBlock Text="{Binding Id}"/>
<TextBlock Text="{Binding ComponentData}"/>
</StackPanel>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>

Add TabItems to an existing TabControl WPF/MVVM

I have TabControl that has already define some TabItems on XAML. I need to create new TabItems and add to it.
If I use ItemSource I get an exception Items collection must be empty before using ItemsSource.
The solution I have found so far is to create those TabItems I have already defined on XAML but programmatically on the ViewModel, so I can created the others I really need, but doesn't seems to be a good solution.
Other solution would be to add the TabControl as a property and use the Code-Behind to bind it to the ViewModel, which I would like to avoid.
So, I'm just wondering if there is a way to do this only with XAML and MVVM.
Edit:
ItemSource attempt, which is working.
XAML:
<TabControl Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Stretch"
BorderThickness="0.5"
BorderBrush="Black"
ItemsSource="{Binding Model.TabItems, Mode=TwoWay}">
<!--<TabControl.Items>
</TabControl.Items>-->
</TabControl>
Model
public ObservableCollection<TabItem> TabItems {get;set;}
VM
TabItem tabItem = new TabItem { Content = new DetailedViewModel((MyObject)inCommandParameter) };
Model.TabItems.Add(tabItem);
What you are doing here is NOT MvvM. Idea behind it is to keep parts of the app separate, i.e. Model should NOT return any UI elements. If you want to use this with any other UI framework for example WinForms then it will fail and will require additional work.
What you need is something like this, bear in mind that this is an example and you will need to modify this to comply with your requirements.
Model class:
namespace Model
{
public class Profile
{
public string Name { get; set; }
public static int I { get; set; } = 2;
}
}
After this you will need the ViewModel:
namespace VM
{
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
ProfilesCollection = new List<Profile>();
for (int i = 0; i < 100; i++)
{
ProfilesCollection.Add(new Profile() {Name = $"Name {i}"});
}
}
private List<Profile> profilesCollection;
public List<Profile> ProfilesCollection
{
get { return profilesCollection; }
set { profilesCollection = value; OnPropertyChanged(); }
}
}
}
Now we have base to work with. After that I assume you know how to add the relevant references in your xaml, but this might be seen by other people so I will include it anyway.
Here is a complete MainWindow.xaml:
<Window x:Class="SO_app.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:vm="clr-namespace:VM;assembly=VM"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:converter="clr-namespace:SO_app.Converters"
xmlns:validation="clr-namespace:SO_app.Validation"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:SO_app"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:model="clr-namespace:Model;assembly=Model"//reference to my model
mc:Ignorable="d"
Title="MainWindow" Height="452.762" Width="525" Closing="Window_Closing">
<!-- d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}" -->
<Window.Resources>
<CollectionViewSource Source="{Binding ProfilesCollection}" x:Key="profiles"/> // this corresponds to our collection in VM
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>//Data Context of the Window
</Window.DataContext>
<Window.Background>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle Width="50" Height="50" Fill="ForestGreen"></Rectangle>
</VisualBrush.Visual>
</VisualBrush>
</Window.Background>
<TabControl>
<TabControl.Resources>
<DataTemplate DataType="{x:Type model:Profile}">//this data template will be used by the TabControl
<Grid>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemsSource>
<CompositeCollection>
<TabItem Header="First Item"/>
<TabItem Header="SecondItem"/>
<CollectionContainer Collection="{Binding Source={StaticResource profiles}}"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
If you want to add more items then just use Command which would be implemented in VM and just add profile to it and enjoy the show.

WPF listbox is causing a stackoverflow when it has a button as a child

When trying to add a button as the datatemplate to a listbox, I ran into a stackoverflow. When using a textbox instead, there is no stackoverflow. What is causing this? I'm using Visual Studios 2012 Update 4.
XAML code:
<Window x:Class="StackOverflowTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<ListBox ItemsSource="{Binding CausesStackOverflow}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type sys:String}">
<Button Content="{Binding Path=.}"/>
</DataTemplate>
</ListBox.Resources>
</ListBox>
</Window>
C# code:
namespace StackOverflowTest
{
public partial class MainWindow
{
public string[] CausesStackOverflow { get; set; }
public MainWindow()
{
CausesStackOverflow = new string[] { "Foo" };
InitializeComponent();
DataContext = this;
}
}
}
Button is a ContentControl, which also uses a DataTemplate for its Content. A default DataTemplate ends up in recursively creating Buttons to display the "outer" Button's Content.
You should set the ListBox's ItemTemplate explicitly:
<ListBox ItemsSource="{Binding CausesStackOverflow}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

ItemControl not binding objects correctly

I have a XAML page that contains an ItemControl tag (the application uses the MVVM light framework):
<ItemsControl MinWidth="100" MinHeight="25" ItemsSource="{Binding Path=Options}" HorizontalAlignment="Left" d:LayoutOverrides="Height" Margin="10,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
This control has an item source that is a list of Option objects. The data template for this item control is as follows:
<DataTemplate DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
I have a view model that is associated with the SalesOptionButton control which is as follows:
public class SalesOptionButton
{
private string _name;
private Option _Option;
public ICommand SelectedOptionButtonCommand { get; set; }
public string Name
{
get { return _name; }
set { SetStructPropertyValue(ref _name, value); }
}
public Option Option
{
get { return _scriptOption; }
set { SetPropertyValue(ref _scriptOption, value); }
}
public SalesScriptOptionButton(ScriptOption option)
{
Option = option;
Name = option.OptionText;
}
protected override void RegisterForMessages()
{
SelectedOptionButtonCommand = new RelayCommand(OptionButtonSelected);
}
private void OptionButtonSelected()
{
MessengerService.Send(ScriptOptionSelectedMessage.Create(ScriptOption));
}
protected override void SetDesignTimeInfo(){}
}
Here is the XAML for the Option Control:
<UserControl [INCLUDES]>
<Button Height="25" Padding="1" MinWidth="100" Content="{Binding Name}" Command="{Binding SelectedOptionButtonCommand}"/>
</UserControl>
What this does is, for every option that is in the datasource, a button is created. These buttons should display the name of the option and, when the button is clicked, send a message to the main application that will process that click (set the chosen option).
The issue that I am seeing is that the buttons are being created but nothing else is being bound (There is no option name being displayed on the button and the button click is not working). Can anyone give me an idea as to why this isnt working like I think that it should be?
You aren't setting the data template as a property of your items control.
<ItemsControl ItemTemplate={StaticResource OptionTemplate} .../>
<DataTemplate x:Key="OptionTemplate" DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
It's difficult to decipher your post when bits and pieces of the code appear to be missing. You say:
This control has an item source that is a list of Option objects. The data template for this item control is as follows:
<DataTemplate DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
You haven't shown us your Option class - only your SalesOptionButton class. Presumably, your Option type has some property that yields the associated SalesOptionButton instance? If that's the case, then your data template is wrong here:
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
You're setting the DataContext of the SalesOptionButton to the Option instance, not to the SalesOptionButton instance. I'm guessing (and I have to) that you want something like this:
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding SalesOptionButton}"/>

Categories