WPF "partial forms" - wpf

I'm making an application for DB migrations. I made a multithreaded framework with WPF GUI. I put someting like this in my namespace/folder:
class Something : Migrator {
public override Run(){
//I would need this
string valueOfMyCustomFieldOnForm = xyz.Text; //example
int count = 500;
for(int i = 0; i < 500; i++){
//do something here
OnProgressChanged(...); //call event, GUI is updated
}
OnCompleted(...); //migration completed
}
}
Then using reflection I put all classes in that namespace onto dropdown list. When I choose one in a list and click Start, the Thread with code in Run method is started.
DB Host: TEXTBOX
DB Username: TEXTBOX
DB Password: TEXTBOX
--
Migrator custom field 1: TEXTBOX
Migrator custom field 2: TEXTBOX
...
--
List with migrated items - irrelevant
There are few commong field on GUI (like database host, username etc...). But for some of those migrators I would need custom fields on GUI (for example 3 extra textbox fields).
What is the best way to do this in WPF? I need part of the GUI to be dynamic.

There's a lot of seemingly-irrelevant information in your question, which - I think - is really about mechanisms for creating metadata-driven UIs in WPF. Here's a way to approach that problem:
Suppose that you want to build a property-sheet-like UI: a grid that displays a row for each property, with a prompt and an input control of some kind. To do this, you're going to need a collection of objects, with each item in the collection including properties that describe the property and its value. A simple design would be a class that exposes a Prompt property and a Value property and that implements change notification.
Once you have created and populated this collection, you can implement an ItemsControl that displays it in a grid:
<ItemsControl ItemsSource="{Binding Properties}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="PropertyViewModel">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Prompt"/>
<ColumnDefinition SharedSizeGroup="Value"/>
</Grid.ColumnDefinition>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
</Grid>
<Label Content="{Binding Prompt}"/>
<TextBox Grid.Column="1" Text="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is pretty simple - the most complicated thing about it is using Grid.IsSharedSizeScope so that all of the grids that this control creates use the same column widths. You could also use a ListView instead of an ItemsControl, though using a ListView for this introduces a bunch of issues surrounding focus and selection that you may not want to deal with.
Note that because of the magic that is WPF template matching, you could conceivably implement the Value property as an object, and create different templates to handle the different possible types of the Value property - just like a real property sheet does. To do this, you'd create a template for each type, e.g.:
<DataTemplate DataType="{x:Type System:String}">
<TextBox Text="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type System:DateTime}">
<DatePicker Value="{Binding Value, Mode=TwoWay}"/>
</DataTemplate>
etc. Then you'd change the template for the PropertyViewModel so that instead of showing the Value in a TextBox, it uses a ContentPresenter, e.g.:
<ContentPresenter Grid.Column="1" Content="{Binding}"/>

Related

WPF binding to a number of objects only known at runtime

I'm trying to understand the limits of binding in WPF (if any). I understand how binding to a pre-defined number of objects in XAML works, e.g.:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding MyText}" Grid.Row="1" Grid.Column="0"/>
</Grid>
(I used TextBlock just as an example, it could have been a Button or any other element)
Now, suppose that, instead of a single TextBlock, I need to display a number of them, but the exact number will only be known at run-time, together with the text to be written in each TextBlock (and possibly other attributes I may want to bind). Is this something that can be achieved in practice?
To display multiple items in WPF, you would typically use the base ItemsControl class or one of the classes that derives from it. Here is a diagram of the inheritance hierarchy, you can use ItemsControl when all you need is basic functionality and one of its derived classes when you need more:
ItemsControl and its children provide an ItemsSource attribute that allows you to bind your collection (usually an ObservableCollection). However, for user-defined types, you will also need to provide a data template to tell the control how to display the contents.
For example, say you had a simple class like the following:
public class Message
{
public string MyText { get; set; }
}
And you create a list of them (in your case you would populate the list at run time):
Messages = new List<Message>
{
new Message { MyText = "SomeText1" },
new Message { MyText = "SomeText2" },
new Message { MyText = "SomeText3" },
};
You could display them all using the following xaml:
<ItemsControl ItemsSource="{Binding Messages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding MyText}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In side the DataTemplate you would add the controls you use to display the properties of your type and bind to them.
NOTES
Please note that the example above is the bare bones implementation just to show how to get started. Once you get more advanced, you may need to implement change notifications for your properties (i.e. INotifyPropertyChanged) and also for adding/removing items for your collection, etc.

ListView Data Binding for Windows 8.1 Store Apps

So I am trying to move a desktop program i made to an app for Windows 8.1 using Visual Studio 2013. Datagrid was deprecated for this release so I am trying to translate those over to a ListView. I previously used ListViews in other desktop applications, but it looks like a lot has changed. The problem I am having is populating my ListView from my database. I am connecting through a WCF Service and stepping through the code, I can see I am getting the right data, I just cant get it to appear in my ListView. My preferred end result would be a 'listview' with 3 columns of information which will be editable. Previously I would use ListView.View and then put a GridView in there for the columns. But it appears ListView.View is deprecated just like GridView.Columns.
here is my xaml for the ListView
<ListView x:Name="lvInventory" Grid.Row="2" Style="{StaticResource listViewStyle}" ItemsSource="{Binding}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="200" Width="200">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="tbName" Text="{Binding InventoryName}" Width="200" Foreground="#FF0E0D0D" />
<TextBox x:Name="tbQty" Grid.Column="1"/>
<TextBox x:Name="tbType" Grid.Column="2"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In my codebehind I am assigning the itemssource of my listview.
VMInventory inventory = new VMInventory();
inventory.GetList();
lvInventory.ItemsSource = inventory;
VMInventory is my ViewModel where I am getting my data from the WCF service and that looks like:
public async Task GetList()
{
this.connection = new InventoryModelEntities(new Uri(url));
var filteredList = from o in connection.Inventory
where o.Have == false
select o;
var query = await Task.Factory.FromAsync((filteredList as DataServiceQuery).BeginExecute(null, null),
(result) => (filteredList as DataServiceQuery).EndExecute(result)) as IEnumerable<Aurora.InventoryService.Inventory>;
this.inventoryList = query.ToList();
this.currentItem = 0;
this.onPropertyChanged("Current");
this.IsAtStart = true;
this.IsAtEnd = (inventoryList.Count == 0);
}
One last side note, I was able to add a textbox to the Grid and when I DataBind it to text="{Binding Current.InventoryName}" I am able to successfully bind it.
If i understand correctly your UI is not getting updated with values what ever your adding in ViewModel, but if you add some value in UI it is reflecting in the ViewModel.
If that is the case, the use ObserableCollection instead of list, and if your creating list every time, then you have to implement INotifyPropertyChanged in your ViewModel.
If you have done all this but still it is not updating then, your creating the list in asy Task, which is not a UI thread. If you want to update a UI from a non-Ui thread, then use update the UI using Dispatcher. You can find lot of examples to update the UI from non-UI thread using dispatcher

Two-way databinding to ObservableCollection on Silverlight TreeView using DragDropTarget

Here's the question at its most basic: how do I listen for an update of what is changing in a TreeView control modified via a DragDropTarget?
So here's my deal: I have a TreeView that holds agenda items. All are of the same data type (WCFAgendaItem), and are loaded into a hierarchy with children expressed as a property ChildItems. The whole thing is wrapped up in an ObservableCollection and bound to the TreeView using MVVM Light. Works great to view. I also want users to be able to use drag and drop to reorder, reorganize and add new items to this agenda coming from a variety of other sources (one example is a ListView of image slides). All new items would also have the same data type of WCFAgendaItem, for consistency's sake and easy serialization.
Here's my issue: dragging and dropping works beautifully on the UI using the Toolkit's drag drop functionality. But I have no idea how to get the ViewModel to understand changes to the contents of the TreeView.
Code from the view (Agenda.xaml):
(up top)
<UserControl.Resources>
<AHHSTeam_SLClassroomManagerMVVM_Helpers_Converters:BooleanVisibilityConverter x:Key="BooleanVisibilityConverter"/>
<sdk:HierarchicalDataTemplate x:Key="hdtAgenda" ItemsSource="{Binding ChildItems, Mode=TwoWay}" >
<Grid HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ImageThumbnailWidth}" />
<ColumnDefinition Width="250" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{Binding ThumbnailURL}" Width="{Binding ImageThumbnailWidth}" Height="{Binding ImageThumbnailHeight}" Visibility="{Binding HasImage, Converter={StaticResource BooleanVisibilityConverter}}" >
<ToolTipService.ToolTip>
<Image Source="{Binding ResizedImageURL}" />
</ToolTipService.ToolTip>
</Image>
<TextBlock Grid.Column="1" Text="{Binding Title}" TextWrapping="Wrap" />
</Grid>
</sdk:HierarchicalDataTemplate>
<Style TargetType="sdk:TreeViewItem" >
<Setter Property="IsExpanded" Value="True" />
</Style>
</UserControl.Resources>
(later on)
<controlsToolkit:TreeViewDragDropTarget Grid.Row="1" Grid.Column="0" x:Name="ddtAgenda" AllowDrop="True"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" >
<sdk:TreeView Width="375" ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Visible" ItemsSource="{Binding DailyAgenda, Mode=TwoWay}" ItemTemplate="{StaticResource hdtAgenda}">
</sdk:TreeView>
</controlsToolkit:TreeViewDragDropTarget>
ViewModel code (AgendaViewModel.cs) --> I tried listening for CollectionChanged, so far that doesn't seem to work
(in constructor)
//add notification of agenda changes
DailyAgenda.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(DailyAgenda_CollectionChanged);
(event)
void DailyAgenda_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
System.Windows.MessageBox.Show("Daily agenda updated, now has " + e.NewItems.Count.ToString() + " top-level elements.");
}
Code from model (WCFAgendaItem.cs)
[ContentProperty("ChildItems")]
public partial class WCFAgendaItem: INotifyPropertyChanged
{
private ObservableCollection<WCFAgendaItem> _childItems = new ObservableCollection<WCFAgendaItem>();
public ObservableCollection<WCFAgendaItem> ChildItems
{
get
{
return _childItems;
}
set
{
_childItems = value;
}
}
...
I am pretty sure that I get that listening for CollectionChanged isn't right in any case, given that this data doesn't just change at the top level. I looked at EventToCommand in Blend (MVVM Light, remember) but the only TreeView-specific event appears to be SelectionChanged, which doesn't seem right either. I looked at putting an EventToCommand trigger on the TreeViewDragDropTarget, but aren't those methods about overriding how the UI interactions happen? I don't think INotifyPropertyChanged on WCFAgendaItem is right for this either: although I'm going to want that later for editing item titles, it doesn't seem like it'll help me when items get moved around.
Maybe what I'm looking for is a stretch, but what I really want to have happen is for Silverlight to understand that the databinding works both ways on the ordering and contents of the WCFAgendaItem collection, and do all the collection reworking itself based on UI interactions. Then I could just listen for an update event after the collection is reworked - after that I can just crawl the modified ObservableCollection bound to the TreeView, and flatten/serialize/update via WCF.
Failing the ideal situation: I'm willing to crawl TreeViewItems if need be, but even if that's what I need to do I'm stuck on when to do it. Plus I need a way to pass all that back to the ViewModel so I'm not writing code behind. Do I need to attach to Drop() and rework the dropping logic? I found several old articles about custom drag drop implementations starting from the Toolkit, but nobody mentions how to save out the modified TreeView, especially in an MVVM situation.
finally {
While typing this out I found this article which may be useful, though that's a fair amount of work in the ViewModel. This is promising and I'll investigate, but I'm still holding out hope for something simpler. Also it looks like the Toolkit events have changed a little since the article was written.
}
I also had issues implementing this type of DragDrop functionality. The root cause seemed to be that neither the ItemDragCompleted event (EventHandler) nor ItemDroppedOnSource (DragEventHandler) pass the index at which the item was dropped.
I ended up subclassing the DragDropTarget in order to expose the protected method:
int GetDropTargetInsertionIndex(TItemsControlType dropTarget, DragEventArgs args)
I then used an attached behavior to assume responsibility for inserting items at the specified index into to the underlying collections.
I'm afraid the underlying code is too expansive to include in a StackOverflow answer (mainly due to extensive decoupling) but it's on my list of subjects to blog about. In the mean time, I hope the information above helps, it was certainly the key to the solution for me.
Ian

Databinding between XML file and GUI

I've got a problem with my little app here that is conceptually very simple. I have an XML file that essentially just maps strings to strings.
Long-winded explanation warning
A sample file would look something like this:
<Candies>
<Sees>Box1</Sees>
<Hersheys>Box2</Hersheys>
<Godiva>Box3</Godiva>
</Candies>
Although I could use a different schema, like:
<Candies>
<Candy>
<Name>Sees</Name>
<Location>Box1</Location>
</Candy>
</Candies>
...I opted not to, since the former didn't have any forseeable adverse side effects.
In code behind, I load the contents of my XML file into an XDocument with LINQ. I also have a List variable defined, because this is what I'm databinding my GUI to. CandyLocation looks like this:
public class CandyLocation
{
public string Brand { get; set; }
public string Location { get; set; }
}
And my simple GUI is just this:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<Page.Resources>
<DataTemplate x:Key="CandyTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Brand}" Margin="3"></TextBox>
<ComboBox Grid.Column="1" SelectedValue="{Binding Location}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}, Path=DataContext.LocationNames}" Text="{Binding Location, Mode=TwoWay}" Margin="3"></ComboBox>
</Grid>
</DataTemplate>
</Page.Resources>
<DockPanel>
<Button DockPanel.Dock="Bottom" Margin="3" Command="{Binding SaveCandiesCommand}">Apply Changes</Button>
<Button DockPanel.Dock="Bottom" Margin="3" Command="{Binding AddNewCandyCommand}">Add Candy</Button>
<ListBox DockPanel.Dock="Top" ItemsSource="{Binding CandyLocations}" ItemTemplate="{StaticResource CandyTemplate}" />
</DockPanel>
</Page>
So the overview is this:
The application loads and then uses LINQ to load the XML file. When the GUI is presented, it calls "GetCandyLocations", which traverses the XML data and populates the List object with the contents of the XML file. Upon initial loading of the XML, the GUI renders properly (i.e. the candy brands and their locations appear correctly), but that's where the success story ends.
If I start from a blank XML file and add a brand, I do so by adding a new XElement to my XDocument root. Then I call OnPropertyChanged( "CandyLocations") to make the GUI update. The initial value for Location is "", so it's up to the user to select a valid location from the combobox. The problem is, I can't figure out how to get their selection databound correctly, such that I can update the XElement value. Because of this, when I save the candy locations, everything ends up with a blank location value. In addition, anytime the user clicks Add Candy, all of the previously selected location comboboxes get blanked out.
In summary:
How should I handle the selection change in the GUI? I am using MVVM for this application, so I have avoided using the ComboBox's SelectionChanged event.
Is there a way to databind directly from the GUI to the XDocument? I haven't tried it yet, but it would be best to avoid having multiple sources of data (i.e. XDocument for serialization and List for GUI rendering). Perhaps I can have the getter return the result of a LINQ query and pair it with a value converter???
How would you change my implementation if you were to write this application? I'm still learning MVVM and WPF, so any advice would be really great.
Thanks!
On your ComboBox, it looks like you might be getting a conflict between the SelectedValue and Text properties. Text is usually only used with IsEditable="True". Try using just SelectedItem:
<ComboBox SelectedItem="{Binding Location}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}, Path=DataContext.LocationNames}" ></ComboBox>
If you want to use the XDocument directly as your data source you can use this (assuming XDocument is exposed from the VM as AvailableLocations):
<ComboBox ItemsSource="{Binding Path=AvailableLocations.Root.Elements}" SelectedValue="{Binding Location}"
SelectedValuePath="Value" DockPanel.Dock="Top" DisplayMemberPath="Value"/>
If you'd rather do something like display the company names, just change DisplayMemberPath to "Name".
Also try using an ObservableCollection instead of a List for CandyLocations so you can get automatic change notifications when items are added or removed.

How to Clone a whole grid of Controls?

I have the following code and basically what i am not able to figure out is how to clone the whole grid and make a blank copy of them side by side.... for a clear understanding this is something to do with hospital application and the grid is related to a pregnancy so when said 'ADD CHILD' button a whole new grid should be created during run time, thanks for the help below is a link that might help people cause i tried it but not sure how to display it
How can you clone a WPF object?
You should put the object you are want to "clone" in a DataTemplate and reference this template from an ItemsControl, then when you need another grid add another item to the items control (or even better to the list the control is bound to) and the ItemsControl will create a new grid and bind it the new object.
For an example take a look at this post on my blog.
Here is an example for this application (I left only the relevant parts and I didn't test it, so there are probably some typos there):
<Window ... >
<Window.Resources>
<DataTemplate x:Key="ChildTemplate">
<Grid>
...
<TextBlock Text="Delivery Date:" Grid.Column="0" Grid.Row="0"/>
<TextBox Text="{Binding DeliveryDate}" Grid.Column="1" Grid.Row="0"/>
<TextBlock Text="Delivery Time:" Grid.Column="0" Grid.Row="1"/>
<TextBox Text="{Binding DeliveryTime}" Grid.Column="1" Grid.Row="1"/>
...
</Grid>
</DataTemplate>
</Window.Resources>
...
<Button Content="AddChild" Click="AddChildClick"/>
...
<ScrollViewer>
<ItemsControl ItemsSource="{Binding AllChildren}" ItemsTemplate="{StaticResource ChildTemplate}">
<ItemsControl.PanelTemplate>
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemPanelTemplate>
<ItemsControl.PanelTemplate>
</ScrollViewer>
...
</Window>
And in cs:
Set an object with all the form data as the Window's DataContext. I'll call this class PostDelveryData.
Create another class with the repeating data. I'll call it ChildDeliveryData.
Add a property of type ObservableCollection<ChildDeliveryData> called AllChildren to PostDeliveryData; it's important it'll be ObservableCollection and not any other type of collection.
Now, for the magic:
private void AddChildClick(object sender, RoutedEvetnArgs e)
{
((PostDeliveryData)DataContext).AllChildren.Add(new ChildDeliveryData());
}
And when you add the new item to the list another copy of the entire data template will be added.
I'm not sure that you're using the correct approach here. I would approach the problem by creating a "ChildGridControl" with a Child property, and let the Child property handle the databinding. Adding a new child to the GUI would involve creating a new instance of the ChildGridControl.
If I am understanding correctly, you should create a UserControl, which wraps your Grid and subsequent controls inside. And use this User control anywhere you wanted to replicate that UI.

Resources