I'd like to dynamically generate some controls in my silverlight application.
To be more clear, here's a simplified definition of my class:
public class TestClass
{
[Display(Name="First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
public List<CustomProperty> CustomProperties { get; set; }
}
Each "CustomProperty" will finally be a TextBox, CheckBox or ComboBox:
public class CustomProperty
{
public CustomDataType DataType { get; set; } //enum:integer, string, datetime, etc
public object Value { get; set; }
public string DisplayName { get; set; }
public string Mappings { get; set; } // Simulating enums' behavior.
}
What is the best way to implement this using MVVM pattern? If I parse CustomProperties in ViewModel, and find out which controls should be created, How can I create new controls in my view based on MVVM pattern.
Is there any silverlight control that can help me make the UI faster?
Can I define data annotations programmatically? for example after parsing the custom property, can I add some data annotations (Display, Validation) to the property and bind it to a DataForm, PropertyGrid or a useful control for this situation?
Thank you.
In these cases you usualy use one of the controls inheriting from ItemsControl (e.g. ListBox) or the ItemsControl directly. The controls inheriting from ItemsControl allow you to define a template for each item in a collection, e.g. using your sample (assuming you got access to your TestClass through a view model):
<ListBox ItemsSource="{Binding TestClass.CustomProperties }">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<!--DataContext is stet to item in the ItemsSource (of type CustomProperty)-->
<StackPanel>
<TextBlock Text="{Binding DisplayName}"/>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This snippet creates a ListBox that contains a label and a text box for each CustonProperty in your CustomProperties collection.
Related
I've tried to get at this problem from a few angles. Here I've tried to simplify it into a small test case.
I'm having problems getting a DataGrid cell to update which is bound to a property of a property. The property is set by a bound ComboBox cell in another column. The bound object is a follows, with the property I'm referring to:
public class MainObject : INotifyPropertyChanged
{
private int _subObjectId;
public virtual SubObject SubObjectObj { get; set; }
public int SubObjectId {
get { return _subObjectId; }
set { _subObjectId = value; SubObjectObj = <GetObjFromDB> };
}
...
}
public class SubObject : INotifyPropertyChanged
{
public int Id { get; set; }
public string Name { get; set; }
public string Specialty{ get; set; }
...
}
The DataGrid ItemsSource is
public ObservableCollection<MainObject> SourceData;
Now, the column in the DataGrid is a ComboBox of SubObject choices. A TextBox column next to it which is (supposed) to display the SubObject.Specialty of whatever SubObject is selected in the ComboBox.
<DataGridTemplateColumn Header="SubObjects">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding SubObject.Name, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="ComboBoxSubObject" ItemsSource="{Binding Model.SubObjects, RelativeSource={RelativeSource AncestorType={x:Type uch:TestControl}}}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding SubObjectId, UpdateSourceTrigger=PropertyChanged}"
SelectionChanged="ComboBoxDoctor_OnSelectionChanged"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Specialty" Binding="{Binding Path=SubObjectObj.Specialty}"/>
When the grid is initially painted, the Specialty column is correct - it's the property is what SubObject is displayed in the other column. But when I change the ComboBox, the Specialty column does not change. Is there anyway to tell the DataGrid that the Specialty column binding source has changed and to refresh?
Thanks for any advice.
Is there anyway to tell the DataGrid that the Specialty column binding
source has changed and to refresh?
Yes, this is where your INotifyPropertyChanged implementation comes into play. You should have an OnPropertyChanged event as part of that implementation, invoking this event with a property name tells WPF that the property value has changed and to update the UI. You should call OnPropertyChanged for the Speciality property when your SubObject changes. Because they're in different classes, you'll probably need to expose a method or an event to do this:
public class SubObject : INotifyPropertyChanged
{
public int Id { get; set; }
public string Name { get; set; }
public string Specialty{ get; set; }
public void OnSpecialityChanged()
{
OnPropertyChanged("Speciality");
}
}
public class MainObject : INotifyPropertyChanged
{
private int _subObjectId;
public virtual SubObject SubObjectObj { get; set; }
public int SubObjectId
{
get { return _subObjectId; }
set
{
_subObjectId = value;
SubObjectObj = <GetObjFromDB>
SubObjectObj.OnSpecialityChanged();
}
}
}
Side point, I'm unsure of what use your SubObjectId property is serving here. Could you instead maybe use the Id property directly from the SubObjectObj?
I wonder how I can show design time data in Expression Blend that is located inside a SampleData.xaml using a CollectionViewSource? Before changing my code to use the CVS, I used an ObservableCollection. I was in the need to filter and sort the items inside there, thus I changed the code to use the CVS. Now my designer complains about not being able to fill the SampleData's NextItems with a proper structure to show up in Expression Blend. Here is some code I use inside the app:
MainViewModel.cs
class MainViewModel
{
public MainViewModel()
{
AllItems = new ObservableCollection<ItemViewModel>();
NextItems = new CollectionViewSource();
NextItems.Source = AllItems;
}
public CollectionViewSource NextItems
{
get;
private set;
}
public ObservableCollection<ItemViewModel> AllItems
{
get;
private set;
}
some functions to fill, filter, sort etc...
}
MainView.xaml:
<phone:PhoneApplicationPage
... some other stuff ...
d:DesignWidth="480"
d:DesignHeight="728"
d:DataContext="{d:DesignData SampleData/SampleData.xaml}">
<Grid
x:Name="LayoutRoot"
Background="Transparent">
<controls:Panorama>
<controls:PanoramaItem>
<ListBox ItemsSource="{Binding NextItems.View}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Image}" />
<StackPanel>
<TextBlock Text="{Binding FullName}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
</controls:Panorama>
</Grid>
</phone:PhoneApplicationPage>
SampleData.xaml
<local:MainViewModel
xmlns:local="clr-namespace:MyAppNamespace"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:swd="clr-namespace:System.Windows.Data;assembly=System.Windows" >
<local:MainViewModel.AllItems>
<local:ItemModel
FullName="Dummy"
Image="/Images/dummy.png" />
</local:MainViewModel.AllItems>
<local:MainViewModel.NextItems>
How to fill the CollectionViewSource's Source?
</local:MainViewModel.NextItems>
</local:MainViewModel>
So the question I can't find an answer to is how to fill the Source for NextItems in SampleDate.xaml? Any help would be much appreciated.
if you want to show sample data in the designer I would recommend you to do it from code. There are two ways of generating sample data for the Blend Designer or the VStudio designer:
From an XML file as you do.
From a c# class -> Best option
best option.
In WPF, in windows 8 and in WP7.5 and highger, you can access a propertie called:Windows.ApplicationModel.DesignMode.DesignModeEnabled making use of it you can seed your ObservableCollection from your view model:
public class MainViewModel
{
public MainViewModel()
{
AllItems = new ObservableCollection<ItemViewModel>();
if (DesignMode.DesignModeEnabled)
{
AllItems = FakeDataProvider.FakeDataItems;
}
NextItems.Source = AllItems;
}
public CollectionViewSource NextItems
{
get;
private set;
}
public ObservableCollection<ItemViewModel> AllItems
{
get;
private set;
}
}
In this way, if you change the model, you dont' have to regenerate an XML file, it's a little bit cleaner from a C# file. The FakeDataProvider is an static class where all design-time fake data are stored. So in you XAML the only thing you have to do is to bind your Listbox to the collection of your ViewModel.
I have a datagrid that potentially can have many rows. As the user right clicks one of the rows, I need to show a context menu for each of the rows and perform an action (same action but different data item according to the current selected row) when the user clicks the option.
What is the best strategy for this?
I'm fearing that a ContextMenu for each row is overkill even though I'm creating the menu using the ContextMenuOpening event, sort of a "lazy load" for the context menu. Should I only use one ContextMenu for the datagrid? But with this I would have some more work regarding the click event, to determine the correct row, etc.
As far as I know, some of the actions will be disabled or enabled depending on the row, so there is no point in a single ContextMenu for a DataGrid.
I have an example of the row-level context menu.
<UserControl.Resources>
<ContextMenu x:Key="RowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Edit" Command="{Binding EditCommand}"/>
</ContextMenu>
<Style x:Key="DefaultRowStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />
</Style>
</UserControl.Resources>
<DataGrid RowStyle="{StaticResource DefaultRowStyle}"/>
The DataGrid must have a binding to a list of view models with commands:
public class ItemModel
{
public ItemModel()
{
this.EditCommand = new SimpleCommand
{
ExecuteDelegate = _ => MessageBox.Show("Execute"),
CanExecuteDelegate = _ => this.Id == 1
};
}
public int Id { get; set; }
public string Title { get; set; }
public ICommand EditCommand { get; set; }
}
The context menu is created in the resources collection of the UserControl and I think there is only one object which is connected with datagrid rows by reference, not by value.
Here is another example of ContextMenu for a Command inside a MainViewModel. I suppose that DataGrid has a correct view model as the DataContext, also the CommandParameter attribute must be placed before the Command attribute:
<ContextMenu x:Key="RowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Edit" CommandParameter="{Binding}"
Command="{Binding DataContext.DataGridActionCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}" />
</ContextMenu>
Models:
public class MainViewModel
{
public MainViewModel()
{
this.DataGridActionCommand = new DelegateCommand<ItemModel>(m => MessageBox.Show(m.Title), m => m != null && m.Id != 2);
}
public DelegateCommand<ItemModel> DataGridActionCommand { get; set; }
public List<ItemModel> Items { get; set; }
}
public class ItemModel
{
public int Id { get; set; }
public string Title { get; set; }
}
But there is a problem that MenuItem isn't displayed as a disabled item if CanExecute returns false. The possible workaround is using a ParentModel property inside the ItemModel, but it doesn't differ much from the first solution.
Here is example of above-described solution:
public class ItemModel
{
public int Id { get; set; }
public string Title { get; set; }
public MainViewModel ParentViewModel { get; set; }
}
//Somewhere in the code-behind, create the main view model
//and force child items to use this model as a parent model
var mainModel = new MainViewModel { Items = items.Select(item => new ItemViewModel(item, mainModel)).ToList()};
And MenuItem in XAML will be simplier:
<MenuItem Header="Edit" CommandParameter="{Binding}"
Command="{Binding ParentViewModel.DataGridActionCommand}" />
Given the following XAML snippet:
<UserControl.Resources>
<ResourceDictionary>
<Style x:Key="ContextMenuItemStyle">
<Setter Property="MenuItem.Header" Value="{Binding Text}"/>
<Setter Property="MenuItem.ItemsSource" Value="{Binding Children}"/>
<Setter Property="MenuItem.Command" Value="{Binding Command}" />
</Style>
<ContextMenu x:Key="contextMenu" ItemsSource="{Binding MenuOptions}" ItemContainerStyle="{StaticResource ContextMenuItemStyle}" />
</ResourceDictionary>
</UserControl.Resources>
<DockPanel>
<TextBox Height="30" DockPanel.Dock="Top" ContextMenu="{StaticResource contextMenu}" />
<Button Content="Add Menu Item" DockPanel.Dock="Top" Command="{Binding AddMenuItem}" />
</DockPanel>
And View Model:
public class MyViewModel {
public ObservableCollection<MenuItem> DocumentExplorerMenuOptions { get; set; }
MenuItem firstMenuItem;
MenuItem secondMenuItem;
public MyViewModel() {
firstMenuItem = new MenuItem("First") { Command = new DelegatingCommand(x => MessageBox.Show("First Selected") };
secondMenuItem = new MenuItem("Second") { Command = new DelegatingCommand(x => MessageBox.Show("Second Selected") };
MenuOptions = new ObservableCollection<MenuItem> { firstMenuItem, secondMenuItem };
AddMenuItem = new DelegateCommand<object>(x => firstMenuItem.Children.Add(
new MenuItem("Child of First")));
}
public DelegateCommand<object> AddMenuItem { get; set; }
}
And class:
public class MenuItem {
public MenuItem(string text) {
Text = text;
Children = new List<MenuItem>();
}
public string Text { get; set; }
public List<MenuItem> Children { get; private set; }
public ICommand Command { get; set; }
}
Clicking the button does add the child to firstMenuItem but it does not appear in the context menu of the TextBox.
I can't figure out how to make the context menu show the dynamic content of the context menu. Any thoughts?
I would not bind to a collection of MenuItems but rather to a more data-driven collection which may contain the MenuItem header, a command which is executed upon click and another collection of such items for the sub-items. Then you could use a (Hierarchical)DataTemplate to generate the menu on the fly. Doing so would probably take care of update issues if your datatype implements the necessary interfaces.
Edit: You seem to have such a datatype already, could you post its code?
Edit2: I think the problem is that you use a style that explicitly needs to be applied (it is probably only being applied to the main context menu, not the sub-items), as noted before i'd suggest a HierarchicalDataTemplate.
Edit3: lol...
public List<MenuItem> Children { get; private set; }
Of course it's not going to update if it's a List and not an ObservableCollection.
(The class is quite badly designed overall by the way, Lists should normally not even have a private setter, they should be properties with just a getter to a readonly field)
Assuming the following view model definition:
public class MyObject {
public string Name { get; set; }
}
public interface IMyViewModel {
ICommand MyCommand { get; }
IList<MyObject> MyList { get; }
}
And a UserControl with the following code behind:
public class MyView : UserControl {
public IMyViewModel Model { get; }
}
If my XAML looked like this:
<UserControl>
<ListBox ItemsSource="{Binding MyList}">
<ListBox.ItemTemplate>
<TextBlock Text="{Binding Name}" />
<Button Content="Execute My Command" cmd:Click.Command="{Binding Path=MyCommand, ?????????}" cmd:Click.CommandParameter="{Binding}" />
</ListBox.ItemTemplate>
</ListBox>
How can I bind my Button to the ICommand property of my code-behind class?
I'm using Prism and SL 3.0 and I need to bind each button in my list box to the same command on my view model.
Before my UserControl had a specific name and I was able to use the ElementName binding, but now my UserControl is used multiple times in the same parent view so I can't use that technique anymore and I can't figure out how to do this in XAML.
If it is my only option I can do it manually in the code-behind, but I'd rather do it declaratively in the XAML, if possible.
You need a DataContextProxy for this to work because you're no longer in the context of the UserControl. You've moved out of that and there is no good way to reach back into that context without something like the DataContextProxy. I've used it for my projects and it works great.