Reputation: 3601
I am developing a custom control. My Custom Control has a listview in it which is suppose to show multiple fields from the binding EF Entity. The output would be something like this
Now what would be the data source for listview as all my Entities has different properties so i am not sure about the binding properties.
Right now my ListView has one image control, two textblock and one link labels, how should i determine with which control which property should bind. >
I like to use this control in a RecordListing screen, say 'Client screen bind to Client entity' and 'Employee screen bind to Employee entity'.There should be one entity at a time in my control,
Kindly guide me how could i do this is a very genric and logical way.
Thanks
Upvotes: 3
Views: 7142
Reputation: 1868
Like Foovanadil suggested, I would expose a DataSource DependencyProperty. But over and above that I would also expose 5 more string dependency properties. The consumer of the control would then put the names of the properties they want to go in the specific controls.
Let me be more specific:
Think about how a Combobox works, where you can bind their DataSource, but you can also supply a DisplayMemberPath
and a SelectedValuePath
that specify which properties in the datasource to use.
You could do the same thing with your control:
Expose an "ImagePathMember" property. This will be the Name of the property that contains the path for the image that goes in the image control
Expose a "LinkPathMember" property. This property will be the Name of the property that contains the link's path
Expose a "LinkDisplayMember" property. This property will be the Name of the property that contains the text that the link will look like.
Expose a "TopTextBlockMember" property. This property will be the Name of the property that contains the text for the top textblock
Expose a "BottomTextBlockMember" property. This property will be the Name of the property that contains the text for the bottom textblock
Then you just use reflection in the control to determine the value for each listbox item and then bind it to the listboxitem's image control, link, and 2 textblocks
Hope that helps
u_u
Ok you asked for some code to point you in the right direction.
public static DependencyProperty ImagePathMemberProperty = DependencyProperty.Register("ImagePathMember", typeof(string), typeof(MyCustomControl), new PropertyMetadata("",ImagePathMemberPropertyChanged));
public static DependencyProperty DataSourceProperty = DependencyProperty.Register("DataSource", typeof(string), typeof(MyCustomControl), new PropertyMetadata("",DataSourceChanged));
public string ImagePathMember
{
get
{
return (string)GetValue(ImagePathMemberProperty);
}
set
{
SetValue(ImagePathMemberProperty, value);
}
}
public string DataSource
{
get
{
return (string)GetValue(DataSourceProperty);
}
set
{
SetValue(DataSourceProperty, value);
}
}
As it turns out we don't even need reflection. When you use bindings in code, you actually supply a string name for the property, which is handy because the dependency properties we created are really string names of the properties. Lucky us!
private void DataSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
RecreateBindings();
}
//Repeat this for all the dependencyproperties
private void ImagePathMemberPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
RecreateBindings();
}
private void RecreateBindings()
{
//Repeat this for all dependency properties
if(ImagePathMember!=null)
{
Binding ImagePathBinding= new Binding(ImagePathMember);
ImagePathBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
MyImageControl.SetBinding(ImageControl.ImagePathProperty, ImagePathBinding);
}
}
I wrote this code by hand so its probably all buggy. Also I'm a little worried about the binding, I didn't set a Source
because I thought it would bind to the item in the collection, but I'm not sure of this behaviour.
So I'm going to post this now and then give it a test run, and adapt it where required.
Good Luck
u_u
Ok so things turned out to be alot more complex then I imagined. When the TextBlocks are in a DataTemplate you can't really access them by just calling their name in the code behind.
What you have to do is wait for the ListBox/ListView to generate its items' containers, then use the visualtreehelper to loop through all the children of the listview to find the specific controls you are looking for and then bind to them.
This took me such a long time to do because I couldn't find the controls, because I attached an event handler to the ListView's ItemsSourceChanged event, which meant I looked as the ItemsSource property changed, but before the containers for these items was generated.
Eventually I found a solution:
In the template of your ListView/ListBox where you have the controls, you need to name them like so:
<ImageControl x:Name="MyImageControl" [...]></ImageControl>
You also need to give a name to your listbox/listview, like so (and bind its ItemsSource to your DataSource property):
<ListBox x:Name="listbox" ItemsSource="{Binding ElementName=me, Path=DataSource, UpdateSourceTrigger=PropertyChanged}" [...]></ListBox>
You will see that the binding has ElementName=me
. This is because I am binding to the actual control I am in (i.e. the MyCustomControl). My UserControl
has x:Name="me"
above the xmlns
, as this makes it easy for me to bind to properties in the code behind.
You basically need to revamp the RecreateBindings method. I made a big mistake with my first post, as it needs to be a static method in order to run in the DependencyProperty's PropertyChangedCallBack (i really shouldn't have done the code by hand).
This is what I ended up with:
//Repeat this for all types of controls in your listbox.
private static void RecreateImageControlBindings(ListBox listbox, string controlName, string newPropertyName)
{
if (!string.IsNullOrEmpty(newPropertyName))
{
if (listbox.Items.Count > 0)
{
for (int i = 0; i < listbox.Items.Count; i++)
{
ListBoxItem item = listbox.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
if (item != null)
{
Binding imageControlBinding = new Binding(newPropertyName);
imageControlBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
ImageControl t = FindDescendant<ImageControl>(item, controlName);
if (t != null)
BindingOperations.SetBinding(t, ImageControl.ImagePath, imageControlBinding);
}
}
}
}
}
As you can see, now you need a RecreateBindings method for all the different types of controls in your listview/listbox. There is prolly a more generic way of doing it, but you can sort that out yourself. I can't be doing all the work :P
What the code is doing is it goes through the items in the listbox and gets their container. The ImageControl, once generated, will be a child of that container. So we go get the ImageControl via the help of the FindDescendants method I adapted from this post.
Here is that method:
public static T FindDescendant<T>(DependencyObject obj,string objectName) where T : FrameworkElement
{
// Check if this object is the specified type
if (obj is T && ((T)obj).Name == objectName)
return obj as T;
// Check for children
int childrenCount = VisualTreeHelper.GetChildrenCount(obj);
if (childrenCount < 1)
return null;
// First check all the children
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child is T && ((T)child).Name == objectName)
return child as T;
}
// Then check the childrens children
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = FindDescendant<T>(VisualTreeHelper.GetChild(obj, i), objectName);
if (child != null && child is T && ((T)child).Name == objectName)
return child as T;
}
return null;
}
The only adaptation I did was add the check for the name of the control. We have 2 TextBlocks in our ListBoxItem, so the original method would only return the first one. We need to check the names so we can do bindings on both.
So because the RecreateBindings method was split up, we need to change the PropertyChangedCallBacks to call the RecreateBindings methods specific to each property. The datasource property will have all the RecreateBindings methods in it.
private static void DataSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
//Put the RecreateBindings for all the properties here:
RecreateImageControlBindings(((MyCustomControl)sender).listbox, "MyImageControl", ((MyCustomControl)sender).ImagePathMember);
}
//Repeat this for all the dependencyproperties
private void ImagePathMemberPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
RecreateImageControlBindings(((MyCustomControl)sender).listbox, "MyImageControl", ((MyCustomControl)sender).ImagePathMember);
}
Please note here that MyCustomControl is the Type of this control you are creating.
Lastly we need to add a line to the constructor that adds an event handler to the ListBox's ItemContainerGenerator, so we can check when the item container is generated, and we can attach our bindings.:
public MyCustomControl()
{
InitializeComponent();
listview.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (listview.ItemContainerGenerator.Status
== System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
//Do this for all the different bindings we want
RecreateImageControlBindings(listview, "MyImageControl", ImagePathMember);
}
}
That should be it. If you need any help/have any questions let me know
u_u
Upvotes: 6
Reputation: 6501
If this is truly a custom control that is meant to be re-used in multiple places with varying data sources then you would leave it up to the control consumer to set the DataSource.
You will want to add a custom Dependency Property on your custom control called DataSource. This will give the control consumer something to set when they use the control.
Then when somebody uses your control they will do something like this:
<SomeNamespace:YourCustomControl DataSource="{Binding ControlConsumerEFEntity}" />
Again, if this is a true custom control it wouldn't be setting the DataSource of it its internal elements directly. It would leave it up to the control consumer to set this.
Think about how the built in WPF ListBox works. If you just do this:
<ListBox />
There is no DataSource set but if you do
<ListBox DataSource="{Binding MyCollection}" />
Then the ListBox will be given a DataSource to use. Which was specified by the person consuming the ListBox control. Make sense?
Upvotes: 3