Dynamic Collapse-Panel creation in Windows Forms / C# - c#

I need to write a Windows Form where the user can view many "contracts" grouped by "customer". Each customer must be a expand-collapse panel, and the contracts of the customer must be inside of the corresponding panel.
I already tried the great ExpandCollapsePanel, but when the number of customers is big then the panel doesn't autoscroll, even with the AutoScroll property set to true.
Does anybody know some other alternatives? Remember the panels must be dynamically created because there are many customers and many contracts belonging to each customers.

Ok, I've created a sample using an ElementHost to host a WPF UserControl, it looks like this:
I've uploaded the full source code Here, but anyways these are the most relevant parts:
Form1:
public partial class Form1 : Form
{
public CustomerContractsViewModel ContractsVM { get; set; }
public Form1()
{
InitializeComponent();
ContractsVM = new CustomerContractsViewModel();
var customercontractsview = new CustomerContractsView(){DataContext = ContractsVM};
var elementHost = new ElementHost() { Dock = DockStyle.Fill };
elementHost.Child = customercontractsview;
panel1.Controls.Add(elementHost);
}
private void button1_Click(object sender, EventArgs e)
{
ContractsVM.LoadCustomers(DataSource.GetCustomers());
}
}
(Designer code omitted for brevity)
WPF View:
<UserControl x:Class="ElementHostSamples.CustomerContractsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<!-- This style is applied to all Label elements within the UserControl-->
<Style TargetType="Label">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<!-- This DataTemplate will be used to render the Contract items-->
<DataTemplate x:Key="ContractTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Contract Date:"/>
<Label Grid.Row="1" Grid.Column="0" Content="Amount:"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ContractDate, StringFormat='MM/dd/yyyy'}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Amount, StringFormat=C}" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
<!-- This DataTemplate will be used to render the Customer Items -->
<DataTemplate x:Key="CustomerTemplate">
<Expander Header="{Binding Name}">
<ListBox ItemsSource="{Binding Contracts}" ItemTemplate="{StaticResource ContractTemplate}">
<ListBox.Template>
<ControlTemplate TargetType="ListBox">
<ItemsPresenter/>
</ControlTemplate>
</ListBox.Template>
</ListBox>
</Expander>
</DataTemplate>
</UserControl.Resources>
<ListBox ItemsSource="{Binding Customers}"
ItemTemplate="{StaticResource CustomerTemplate}"/>
</UserControl>
Code Behind:
public partial class CustomerContractsView : UserControl
{
public CustomerContractsView()
{
InitializeComponent();
}
}
ViewModel:
public class CustomerContractsViewModel:PropertyChangedBase
{
public List<Customer> Customers { get; set; }
public void LoadCustomers(List<Customer> customers)
{
Customers = customers;
OnPropertyChanged("Customers");
}
}
Notice how this simple, less than 100 lines of code, 20-minute WPF sample is better than anything you can ever hope to achieve in winforms, and doesn't need any "owner draw", "P/Invoke" (whatever that means) or horrendous gargantuan code behind stuff. And does not force you to spend lots of money in third party components such as DevExpress or Telerik. This is why WPF is the best option for ALL .Net Windows Desktop application development, regardless if it's a simple Hello World type of stuff.
I'm using an ItemsControl to host the Customer items, and inside these I'm using a ListBox with a custom DataTemplate to show the Contract items.
Both ItemsControl (outer and inner) are Virtualized to enable an immediate response time, even with 200,000 items.
Notice that there's not a single line of code that interacts with the UserControls' UI Elements, Everything is defined in XAML and populated with data via DataBinding. This enables a great amount of scalability and maintainability because the UI is completely decoupled from the application logic / business logic. That's the WPF way.
The Form code (except for the initialization code) only interacts with the ViewModel, and has no need to interact with the WPF View.
When upgrading from winforms to WPF, you seriously need to embrace The WPF Mentality, which is, as mentioned before, you almost never manipulate UI elements in procedural code, or use too much code behind, but rather use DataBinding for everything and embrace The MVVM Pattern
WPF Rocks. Download the linked source code and see the results for yourself.
Let me know if you need further help.

Related

MVVM how to pass object from main window to user control? [duplicate]

This question already has answers here:
What is DataContext for?
(4 answers)
Closed 4 months ago.
sorry about this question. I know MVVM exist for many years but each time I try to code something with it I face the same issue again and again ans I'm still looking for a real good tutorial about this.
Let's consider we have a main window (MainWindow.xaml) with its view model (MainViewModel.cs).
This window has a grid, in my grid I define 2 user controls. Whatever it is. One is on the left, one on the right. On my main window I have create, in MainViewModel.cs an engine:
internal class MainWindowViewModel
{
public MainWindowViewModel()
{
QCEngine qcEngine = new();
}
}
This engine is my unique model and contains a complex code that read data. Whatever. This engine has a public list of value. I want to display these values on my left and right panels in different ways. Again whatever. The display is not my issue.
My issue is how I pass this list or the entire engine reference to my panels? I'm really lost. I can do this in few seconds with any classic WinForms but I never figure out how to do in MVVM. I'm at this moment where I give up MVVM to do classic WinForms. This time I want to understand.
Can you help me?
My QC engine is a RFID reader. It already works fine as console application. All parameters are in a config file. the idea of the interface is to give more flexibility to the reader. Having a nice result screen, a setting screen, some interactions.
<Window x:Class="Beper.QCTable.Control.View.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:view="clr-namespace:Beper.QCTable.Control.View"
xmlns:viewmodel="clr-namespace:Beper.QCTable.Control.ViewModel"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Window.DataContext>
<viewmodel:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Menu -->
<Menu Grid.Row="0" FontSize="20">
<MenuItem Header="_Menu">
<MenuItem Header="_Advanced"/>
</MenuItem>
</Menu>
<!--Header-->
<StackPanel Grid.Row="1" Background="Orange">
<TextBlock FontSize="20">
Header
</TextBlock>
</StackPanel>
<!--Body-->
<Grid Grid.Row="2">
<view:TabPanel/>
</Grid>
<!--Status Bar-->
<StatusBar Grid.Row="3" FontSize="20">
<StatusBarItem>
Status
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
Focus on tab panel:
public class TabPanelViewModel
{
public ObservableCollection<TabItem> Tabs { get; set; } = new ObservableCollection<TabItem>();
public TabPanelViewModel()
{
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
}
}
I cannot chare the engine code but, really, it is just a list of keys (RFID keys / EPC). This is the only public data. I want to display this list of key by group under my tabs.
Passing "this list or the entire engine reference" to the view defats the purpose of implementing the MVVM design pattern in the first place.
What you should do is to use the engine to prepare and set the state of your app/view in your view model.
The controls in the views should then bind to properties of the view model that contains, and effetively defines, the current state.

DataContext to UserControl [duplicate]

I have been unable to find a clean, simple, example of how to correctly implement a usercontrol with WPF that has a DependencyProperty within the MVVM framework. My code below fails whenever I assign the usercontrol a DataContext.
I am trying to:
Set the DependencyProperty from the calling ItemsControl , and
Make the value of that DependencyProperty available to the ViewModel of the called usercontrol.
I still have a lot to learn and sincerely appreciate any help.
This is the ItemsControl in the topmost usercontrol that is making the call to the InkStringView usercontrol with the DependencyProperty TextInControl (example from another question).
<ItemsControl ItemsSource="{Binding Strings}" x:Name="self" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="v:InkStringView">
<Setter Property="FontSize" Value="25"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<v:InkStringView TextInControl="{Binding text, ElementName=self}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the InkStringView usercontrol with the DependencyProperty.
XAML:
<UserControl x:Class="Nova5.UI.Views.Ink.InkStringView"
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"
x:Name="mainInkStringView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding TextInControl, ElementName=mainInkStringView}" />
<TextBlock Grid.Row="1" Text="I am row 1" />
</Grid>
</UserControl>
Code-Behind file:
namespace Nova5.UI.Views.Ink
{
public partial class InkStringView : UserControl
{
public InkStringView()
{
InitializeComponent();
this.DataContext = new InkStringViewModel(); <--THIS PREVENTS CORRECT BINDING, WHAT
} --ELSE TO DO?????
public String TextInControl
{
get { return (String)GetValue(TextInControlProperty); }
set { SetValue(TextInControlProperty, value); }
}
public static readonly DependencyProperty TextInControlProperty =
DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));
}
}
That is one of the many reasons you should never set the DataContext directly from the UserControl itself.
When you do so, you can no longer use any other DataContext with it because the UserControl's DataContext is hardcoded to an instance that only the UserControl has access to, which kind of defeats one of WPF's biggest advantages of having separate UI and data layers.
There are two main ways of using UserControls in WPF
A standalone UserControl that can be used anywhere without a specific DataContext being required.
This type of UserControl normally exposes DependencyProperties for any values it needs, and would be used like this:
<v:InkStringView TextInControl="{Binding SomeValue}" />
Typical examples I can think of would be anything generic such as a Calendar control or Popup control.
A UserControl that is meant to be used with a specific Model or ViewModel only.
These UserControls are far more common for me, and is probably what you are looking for in your case. An example of how I would use such a UserControl would be this:
<v:InkStringView DataContext="{Binding MyInkStringViewModelProperty}" />
Or more frequently, it would be used with an implicit DataTemplate. An implicit DataTemplate is a DataTemplate with a DataType and no Key, and WPF will automatically use this template anytime it wants to render an object of the specified type.
<Window.Resources>
<DataTemplate DataType="{x:Type m:InkStringViewModel}">
<v:InkStringView />
</DataTemplate>
<Window.Resources>
<!-- Binding to a single ViewModel -->
<ContentPresenter Content="{Binding MyInkStringViewModelProperty}" />
<!-- Binding to a collection of ViewModels -->
<ItemsControl ItemsSource="{Binding MyCollectionOfInkStringViewModels}" />
No ContentPresenter.ItemTemplate or ItemsControl.ItemTemplate is needed when using this method.
Don't mix these two methods up, it doesn't go well :)
But anyways, to explain your specific problem in a bit more detail
When you create your UserControl like this
<v:InkStringView TextInControl="{Binding text}" />
you are basically saying
var vw = new InkStringView()
vw.TextInControl = vw.DataContext.text;
vw.DataContext is not specified anywhere in the XAML, so it gets inherited from the parent item, which results in
vw.DataContext = Strings[x];
so your binding that sets TextInControl = vw.DataContext.text is valid and resolves just fine at runtime.
However when you run this in your UserControl constructor
this.DataContext = new InkStringViewModel();
the DataContext is set to a value, so no longer gets automatically inherited from the parent.
So now the code that gets run looks like this:
var vw = new InkStringView()
vw.DataContext = new InkStringViewModel();
vw.TextInControl = vw.DataContext.text;
and naturally, InkStringViewModel does not have a property called text, so the binding fails at runtime.
You're almost there. The problem is that you're creating a ViewModel for your UserControl. This is a smell.
UserControls should look and behave just like any other control, as viewed from the outside. You correctly have exposed properties on the control, and are binding inner controls to these properties. That's all correct.
Where you fail is trying to create a ViewModel for everything. So ditch that stupid InkStringViewModel and let whoever is using the control to bind their view model to it.
If you are tempted to ask "what about the logic in the view model? If I get rid of it I'll have to put code in the codebehind!" I answer, "is it business logic? That shouldn't be embedded in your UserControl anyhow. And MVVM != no codebehind. Use codebehind for your UI logic. It's where it belongs."
Seems like you are mixing the model of the parent view with the model of the UC.
Here is a sample that matches your code:
The MainViewModel:
using System.Collections.Generic;
namespace UCItemsControl
{
public class MyString
{
public string text { get; set; }
}
public class MainViewModel
{
public ObservableCollection<MyString> Strings { get; set; }
public MainViewModel()
{
Strings = new ObservableCollection<MyString>
{
new MyString{ text = "First" },
new MyString{ text = "Second" },
new MyString{ text = "Third" }
};
}
}
}
The MainWindow that uses it:
<Window x:Class="UCItemsControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:UCItemsControl"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<v:MainViewModel></v:MainViewModel>
</Window.DataContext>
<Grid>
<ItemsControl
ItemsSource="{Binding Strings}" x:Name="self" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="v:InkStringView">
<Setter Property="FontSize" Value="25"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<v:InkStringView TextInControl="{Binding text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
Your UC (no set of DataContext):
public partial class InkStringView : UserControl
{
public InkStringView()
{
InitializeComponent();
}
public String TextInControl
{
get { return (String)GetValue(TextInControlProperty); }
set { SetValue(TextInControlProperty, value); }
}
public static readonly DependencyProperty TextInControlProperty =
DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));
}
(Your XAML is OK)
With that I can obtain what I guess is the expected result, a list of values:
First
I am row 1
Second
I am row 1
Third
I am row 1
You need to do 2 things here (I'm assuming Strings is an ObservableCollection<string>).
1) Remove this.DataContext = new InkStringViewModel(); from the InkStringView constructor. The DataContext will be one element of the Strings ObservableCollection.
2) Change
<v:InkStringView TextInControl="{Binding text, ElementName=self}" />
to
<v:InkStringView TextInControl="{Binding }" />
The xaml you have is looking for a "Text" property on the ItemsControl to bind the value TextInControl to. The xaml I put using the DataContext (which happens to be a string) to bind TextInControl to. If Strings is actually an ObservableCollection with a string Property of SomeProperty that you want to bind to then change it to this instead.
<v:InkStringView TextInControl="{Binding SomeProperty}" />

MvvmCross Wpf View with Sub View

I have a working program in Caliburn Micro but am moving over to MvvmCross. What I have working in Caliburn is a ShellView (Parent) that displays my navigation buttons and a cart. On that view, there is another view which is my selection of the navigation buttons, let's call it ActiveView (the view changes, in Caliburn it was ActiveItem() to change the view).
In MvvmCross, I cannot get this same functionality to work. After 3 days of searching and reading, I need help. Here is the image of the program, Blue outline is ShellView, inside of it Red outline is ActiveView.
What I get with MvvmCross is the ShellView, with no ActiveView. So the parent works, but no child is displayed. I have created a few other MvvmCross apps but they contain no navigation.
I have 2 Code Versions, First works but creates a second Window. Second keeps a single Window, but does not navigate. I need a single window with navigation. I feel I have a core misunderstanding and cannot find a source that explains it.
First Sample Works, but creates 2 Windows. An empty window (from MainWindow.xaml) and a second from ShellView. My assumption for 2 Windows opening when app is ran, is MainWindow being a window, and ShellView also being set to Window in xaml and cs.
Based on MvvmCross Playground.Wpf
<views:MvxWindow
x:Class="MvxKioskMtg.Wpf.Views.ShellView"
xmlns:views="clr-namespace:MvvmCross.Platforms.Wpf.Views;assembly=MvvmCross.Platforms.Wpf"
xmlns:mvx="clr-namespace:MvvmCross.Platforms.Wpf.Binding;assembly=MvvmCross.Platforms.Wpf"
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"
xmlns:local="clr-namespace:MvxKioskMtg.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Background="#3d3d3d"
>
<views:MvxWindow.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="Welcome" Command="{Binding ShowWindowChildCommand1}" Grid.Column="0"/>
<Button Content="Checkout" Command="{Binding ShowWindowChildCommand2}" Grid.Column="1"/>
</Grid>
<ContentPresenter Content="{Binding}" Grid.Row="1" />
</Grid>
</DataTemplate>
</views:MvxWindow.ContentTemplate>
</views:MvxWindow>
ShellView.xaml.cs
public partial class ShellView : MvxWindow
{
public ShellView()
{
InitializeComponent();
}
}
Second Version Layout looks correct, but no navigation. Click buttons has no effect. Adding break point to the Command that changes views and it is never reached.
XAML changed from MvxWindow to MvxWpfView in 4 places, otherwise same as First Version.
<views:MvxWpfView
<views:MvxWpfView.ContentTemplate>
</views:MvxWpfView.ContentTemplate>
</views:MvxWpfView>
ShellView.xaml.cs
public partial class ShellView : MvxWpfView
{
public ShellView()
{
InitializeComponent();
}
}
In the XAML, if I update the DataContect to AncestorType={x:Type views:MvxWpfView} from Window, it does update the View, but the entire View. Not the ContentPresenter space. So I lose my navigation buttons.
Which of these methods is correct, and what am I doing incorrect? Am I totally off base? Thank you for any help and guidance you can provide. I'll happily read any sources.

hide or remove border of wpf button in C# code

I have a stackpanel named "mystack" in my xaml file and I am adding buttons in it dynamically from the .cs file and want to remove the border of buttons in C# .cs file
what I really want is to populate this stackpanel with the buttons coming from a list of string
thanks in advance
xaml:
<Grid HorizontalAlignment="Left" Height="227" Margin="10,10,0,0" Grid.Row="2"
VerticalAlignment="Top" Width="530">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="mystack" HorizontalAlignment="Left" Grid.Row="2"
VerticalAlignment="Top" Width="520"/>
</ScrollViewer>
</Grid>
.cs:
public List<String> Schools()
{
List<String> l = new List<string>();
l.Add("SST");
l.Add("SBE");
l.Add("SSH");
return l;
}
I agree with HighCore, you generally do not want to manipulate the UI elements in your code.
To remove the Border of the buttons you can set a Button's BorderThickness property to "0" in Xaml or to new Thickness(0) in the code-behind.
i.e.
myButton.BorderThickness = new Thickness(0);
EDIT:
Okay, I noticed your updated question. I would create a property that stores your list of schools and bind to it in a way similar to this:
public List<string> Schools
{
get { return _schools; }
set { _schools = value; }
}
Somewhere you need to set the DataContext of the control to your class containing the Schools property. If you are dynamically updating the list of Schools you'll need to implement INotifyPropertyChanged so the UI knows when to update. And then your Xaml would look something like this:
<ItemsControl ItemsSource="{Binding Schools}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" BorderThickness="0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl>
You can't remove button's border like: btn.BorderThicknes=new Thickness(0);
See this: Removing button's border
The fast Fix:
What I had to do to effectively hide the button border - and due to the button control template I believe which utilizes and changes Button border (i.e. even if you remove it it'd draw it on some trigger I believe)...
...was to set BorderBrush="Transparent" as well (I always do BorderThickness as well but I'm guessing it's not needed - only for visual/layout look'n'feel)
i.e. setting thickness alone is not enough.
I'm really not sure that's the bets way to do it, or actually I'm
quite sure there must be something smarter - but I always end up with
that.
The Right Way:
Proper way - and recommended - is to write your own Button template -
based on the Microsoft official one - or base it on it - and do what
you need w/o borders.
For the code behind/C#:
You really don't need that as per your changed question - do what others suggested already
the best way to do this is :
<Style TargetType="Button">
<Style.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="0"/>
</Style>
</Style.Resources>
</Style>
what I really want is to populate this stackpanel with the buttons
coming from a list of string
That's called a ListBox:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" BorderThickness="0"/>
<!-- Whatever other customizations to the button -->
</DataTemplate
</ListBox.ItemTemplate>
</ListBox>
ViewModel:
public class ViewModel
{
public ObservableCollection<string> Items {get;set;}
public ViewModel()
{
Items = new ObservablecCollection<string>();
Items.Add("String1");
Items.Add("String2");
Items.Add("String3");
}
}
You need to learn the MVVM pattern.

Storing properties of objects in C#/WPF

I'm allowing user to drag/drop some objects from a toolbox and of course each object has a unique id. As soon as object is used, let's say placed on a grid or canvas, I need to show its properties so I need an array of objects where each object can hold its own properties.
Can you give me some advice and direction on how to implement a class to handle multiple objects while each object can hold on to let's say 10 properties?
The best solution is to use a PropertyGrid control; your application looks similar to Visual Studio and your implementation will be similar to that.
Have a look at this SO question for available PropertyGrid options you have -
Is there a Property Dialog control that i can use in my WPF App?
Now you can define a class for each control and declare normal CLR properties for that control; properties you don't want to display in PropertyGrid can be marked with BrowsableAttribute and PropertyGrid will honor that.
In case you want more control over what properties are displayed, you can create your own custom attribute and modify PropertyGrid implementation to use that attribute and display properties marked with this attribute.
Can you give me some advice and direction on how to implement a class
to handle multiple objects while each object can hold on to let's say
10 properties?
There is no need for you to implement such a class. The way I would handle this problem would be to have a common base class for all the objects in the toolbox (ToolboxItem for example) which only exposes properties and functionality common to all items in the toolbox.
public abstract class ToolboxItem
{
public string Name { get; set; }
public Point Position { get; set; }
}
You can then derive your specific items from this class E.G. TextToolboxItem and RectangleToolboxItem (or whatever you want). The derived classes can then expose only the properties they require.
public class TextToolboxItem : ToolboxItem
{
public string Text { get; set; }
}
public class RectangleToolboxItem : ToolboxItem
{
public Rect Bounds { get; set; }
}
To store these you could just use a generic collection such as:
ObservableCollection<ToolboxItem> items = new ObservableCollection<ToolboxItems>();
As long as the items derive from ToolboxItem they can all be held within the single collection and the individual properties can all be bound to using WPF's data binding features.
You can then create and expose the data in the following way:
public partial class MainWindow : Window
{
private ObservableCollection<ToolboxItem> items;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
items = new ObservableCollection<ToolboxItem>
{
new TextToolboxItem { Name = "primaryText",
Text = "Hello world",
Position = new Point(40, 130) },
new TextToolboxItem { Name = "secondaryText",
Text = "Hello world (again)",
Position = new Point(200, 30) },
new RectangleToolboxItem { Position = new Point(50,300),
Name = "Rect1",
Bounds = new Rect(0, 0, 150, 85) },
};
}
public ObservableCollection<ToolboxItem> Items { get { return items; } }
}
To display this information in the user interface I would do the following:
Use a grid to split the view into two sections. The first is where the properties of the selected item will be displayed and the second displays the 'design surface'
Use a ContentPresenter to display the properties of the selected item.
Use a ListBox with a custom ItemsPanel and ItemContainerStyle to 'draw' your items onto the design surface.
Use a DataTemplate to tell WPF how to render each item in both the 'property grid' and the 'design surface' (This post describes how to use a different DataTemplate for different objects).
The xaml required to achieve this is shown below:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="7*" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{Binding ElementName=listBox, Path=SelectedItem}"
Margin="5">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type this:TextToolboxItem}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Position}"/>
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type this:RectangleToolboxItem}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Position}"/>
<TextBlock Text="{Binding Bounds}"/>
</StackPanel>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<ListBox x:Name="listBox" Grid.Column="1"
Margin="5" ItemsSource="{Binding Items}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type this:TextToolboxItem}">
<TextBox Text="{Binding Text}"
Margin="10"/>
</DataTemplate>
<DataTemplate DataType="{x:Type this:RectangleToolboxItem}">
<Rectangle Width="{Binding Bounds.Width}"
Height="{Binding Bounds.Height}"
Stroke="DarkRed" Fill="Pink"/>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Position.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Position.Y}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
The end result looks like this:
Notice that the properties of the selected item are shown in the left hand section of the window.
Now this solution is currently very crude but does demonstrate a starting point for you to develop this further. Ideas for improvement include:
Re-factoring the code into a viewModel so that it is MVVM compliant.
Handling drag and drop of the items on the 'design surface'.
Changing the `ContentPresenter' for a property grid to give you much richer support for displaying and editing the properties of the selected object.

Categories

Resources