I'm new to WPF and don't yet have a solid grasp of how things should be done...
I have an xml file that stores config data and I want the data that is in this xml file to be displayed on the gui front end.
I'm currently using an XmlDataProvider that reads in a set of data
The data is roughly like:
<Items>
<Item name="item01">
<Position x="0", y="0"/>
</Item>
<Item name="item02">
<Position x="0", y="0"/>
</Item>
<Item name="item03">
<Position x="0", y="0"/>
</Item>
</Items>
The XmlDataProvider is declared as a resource as follows
<Window.Resources>
<XmlDataProvider x:Key="SiteConfigFile" XPath="SiteConfig" >
</XmlDataProvider>
</Window.Resources>
I then enable a combobox to show each item in the Xml file via a drop down menu using:
<ComboBox Name="ButtonMapping" ItemsSource="{Binding Source={StaticResource SiteConfigFile}, XPath=Items/Item}" DisplayMemberPath="#name" SelectedIndex="0">
This all works fine.
The problem I want to solve now is... depending on which item from the combo box is selected, the corresponding Position element with its 2 attributes need to be shown in atextbox on the gui... do i i need to generate dynamic XPath, that seems a bit messy... what is the best way to do this, I'm out of ideas :(
How about wrapping the TextBox within a couple of panels? See example below. I used an outer panel (Border) whose DataContext is bound to the ComboBox.SelectedItem property. Then, another inner panel (StackPanel) is bound to the element in the XML. Finally within this inner panel, I placed the TextBox control whose Text is bound to #x and #y.
<Border DataContext="{Binding ElementName=ButtonMapping, Path=SelectedItem}">
<StackPanel DataContext="{Binding XPath=Position}">
<TextBlock Text="x:"/>
<TextBox Text="{Binding XPath=#x}"/>
<TextBlock Text="y:"/>
<TextBox Text="{Binding XPath=#y}"/>
</StackPanel>
</Border>
Note though that I used two TextBoxes to display the x and y attributes. If you want to use just one, you'll have to use an IValueConverter to correctly "format" the string you want to show.
Related
I have a wpf window where I am using xml data through an XMLDataProvider. The screen is based on a grid and all the data is being displayed correctly, having defined the xml as follows...
<Grid.DataContext>
<XmlDataProvider x:Name="Data" XPath="Book/Page" />
</Grid.DataContext>
With the xml source being set in code behind as follows...
InitializeComponent();
string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
Data.Source = new Uri(appPath + #"\SNG.xml");
All so good so far. But now I have a need to read one of the elements from the xml file in the code behind. All my searching and the only way I've found to do it is to bind it to an invisible control then read the data out of the control. e.g. to read the BookRef from the xml I have the following in the xaml...
TextBlock Name="BookRefTextBox" Visibility="Hidden" Text="{Binding XPath=#BookRef}"/>
Then in the code behind...
string bookRef = BookRefTextBox.Text;
This works, I can then use the data that came from the xml file... but it really feels like a fudge. Is there a better way to get the value of parts of the xml file from within the code behind section.
EDIT:
Forgot to say that I've also tried putting the XmlDataProvider in Windows.Resources instead of in Grid.DataContext as some examples I've found do.
However I then can't find a way to set the path to the xml file in code behind. Added to which putting it in Windows.Resource does not make it any easier to find how to access the data from the Xml file.
EDIT2:
Here is an example of the XML file. Note there are multiple books.
<Books>
<Book Id="1" BookRef="12345" Name="My Book Name" Author="Author" Pages="2" >
<Page PageId="1"/>
<Page PageId="2"/>
</Book>
<Book Id="1" BookRef="67890" Name="My Second Book Name" Author="Author 2" Pages="1" >
<Page PageId="1"/>
</Book>
</Books>
OK, here is another way, more complicated, though.
You are correct that you need to synchronize the currently displayed page with the text of the BookRefTextBox. So on top of XmlDataProvider, I define a CollectionViewSource, which can be used to keep track of the current displayed page.
In the XAML code below, both BookRefTextBox and listBox1 (I use ListBox to display pages) are bound to the same CollectionViewSource, and by setting IsSynchronizedWithCurrentItem="True", the current item is updated when the selected page is changed.
The interesting point is the XPath expression for BookRefTextBox.Test, XPath=../#BookRef means the Text of BookRefTextBox is Find the parent element of the current page- which is Book, and display its BookRef attribute".
The whole XAML of the window.
<Window x:Class="MyNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<XmlDataProvider x:Key="userDataXmlDataProvider1" Source="/Data/XMLFile1.xml" XPath="Books/Book/Page"/>
<CollectionViewSource x:Key="userDataCollectionViewSource1" Source="{StaticResource userDataXmlDataProvider1}"/>
</Window.Resources>
<Grid DataContext="{StaticResource userDataXmlDataProvider1}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<TextBlock x:Name="BookRefTextBox" Grid.Row="0" Text="{Binding XPath=../#BookRef}" />
<ListBox x:Name="listBox1" Grid.Row="1"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="8,0,8,0">
<Label Content="{Binding XPath=#PageId}" Width="100" Margin="5" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
And from code behind, you can get the text of the BookRefTextBox.
Edit:
In order to set the source from code behind:
If XmlDataProvider is declared in Window.Resources, it has a x:Key attribute, you access a resource via Key, not Name.
XmlDataProvider xdp = this.Resources["userDataXmlDataProvider1"] as XmlDataProvider;
xdp.Source = new Uri(...);
I believe I have finally found the answer that avoids the use of a hidden control. First off many thanks to kennyzx for his answer which while it still used a hidden control was invaluable in leading me to this answer.
Instead of putting the XmlDataProvider in the Grid.Context it has been moved to the Window.Resources and a CollectionViewSource added to accompany it.
<Window.Resources>
<XmlDataProvider x:Name="books" x:Key="bookData" XPath="Books/Book/Page"/>
<CollectionViewSource x:Key="collBookData" Source="{StaticResource bookData}"/>
</Window.Resources>
A new XmlDataProvider is defined in the code behind and in the constructor of the window is set to be a reference to the one defined in the XAML Windows.Resources.
XmlDataProvider bookData;
public BookPage()
{
InitializeComponent();
string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
bookData = (XmlDataProvider)this.Resources["bookData"];
bookData.Source = new Uri(appPath + #"\SNG.xml");
}
The DataContext of the Grid is set to be the CollectionViewSource.
<Grid.DataContext>
<Binding Source="{StaticResource collBookData}"/>
</Grid.DataContext>
The above is not 100% necessary as it could be specified on each control instead, but this way makes for simpler binding on each control on the form. (No hidden controls in this solution, only the ones I want to actually show). For example...
<TextBlock Name="myTextBlockName" Style="{StaticResource MyTextBlockStyle}" Text="{Binding XPath=../#BookRef}" />
Finally the bit to read the data from the XML in code behind.
XmlNode currentXmlNode = bookData.Document.SelectNodes("Books/Book/Page").Item(collBookData.View.CurrentPosition);
string currentBookRef = currentXmlNode.ParentNode.Attributes["BookRef"].Value;
Just as an aside, this solution also allows me to use MoveCurrentToPrevious and MoveCurrentToNext against collBookData.View to change the current page being displayed (previously had a hidden listbox control to do that and wasn't happy with that solution either).
This is a two-parter.
First, in WPF the standard ListBox control automatically supports inline searching of its items. It does this via using the items' ToString function, meaning if you have the focus inside a listbox and just start typing, it will do a left-most search, highlighting any items whose ToString matches what you've typed. Subsequent key presses within a short period of time add to the search string (i.e. typing 'A' followed by 'S' will left-search for 'AS' whereas typign 'A' then pausing, then typing 'S' will instead left-search for 'S'.
The problem is this mechanism seems to be dependent solely on the value returned by ToString, which in some cases is something we can't rely on. Is there something else we can use instead of ToString?
The second part of this is that behavior only seems to be present in a ListBox, but no other ItemsControl objects (or hierarchical ones like a TreeView.) Without having to re-write that functionality from scratch, is there an easy way to add it to an ItemsControl?
You can control what is searched with TextSearch.Text or TextSearch.TextPath attached properties. (See http://msdn.microsoft.com/en-us/library/system.windows.controls.textsearch(v=vs.110).aspx)
You can apply TextSearch.TextPath to your ListBox instance (so it searches this property instead of ToString) or you can apply TextSearch.Text to individual ListBoxItem child (so you can set individual search text for individual elements).
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<XmlDataProvider x:Key="Items" XPath="People">
<x:XData>
<People xmlns="">
<Person Name="John" Surname="Smith" />
<Person Name="Andrew" Surname="Johnson" />
<Person Name="Otis" Surname="Everett" />
<Person Name="Jesus" Surname="Osborn" />
</People>
</x:XData>
</XmlDataProvider>
</Window.Resources>
<StackPanel>
<TextBlock Text="Searches by a property (Name):" />
<ListBox ItemsSource="{Binding Source={StaticResource Items}, XPath=*}"
TextSearch.TextPath="#Name">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding XPath=#Name}" /> <Run Text="{Binding XPath=#Surname}" />
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock>Searches by a individual value (number in english):</TextBlock>
<ListBox>
<ListBoxItem TextSearch.Text="One">1</ListBoxItem>
<ListBoxItem TextSearch.Text="Two">2</ListBoxItem>
<ListBoxItem TextSearch.Text="Three">3</ListBoxItem>
<ListBoxItem TextSearch.Text="Four">4</ListBoxItem>
</ListBox>
</StackPanel>
</Window>
This behavior is implemented in the ItemsControl class (and you can find other examples of ItemsControl descendants with search: ComboBox, DataGrid). You can set IsTextSearchEnabled property to true to make it work. (See http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.istextsearchenabled(v=vs.110).aspx)
The single level search works for TreeView. I think you should implement the search programmatically if you want to perform multi level search. (see http://social.msdn.microsoft.com/Forums/vstudio/en-US/e6d58fcc-4eaa-4bdc-8621-ce24c8efd330/treeview-textsearch)
I'm trying to bind to some XML data from my WPF application. I've set up the data context so that the XmlElement I'm trying to bind to ends up looking like this:
<Item name="Potion" classes="Healing Item" value="200">
<Classes>
<Class value="HealingItem" />
</Classes>
<Description value="A low quality potion, it restores a small amount of health" />
<Components>
<HealingComponent>
<BattleHealingComponent>
<HPHealingComponent value="500" type="Absolute"/>
</BattleHealingComponent>
</HealingComponent>
</Components>
</Item>
Now here's the problem. I can't figure out an XPath query I can bind with that will return only the Component Subnodes.
I know it'll go something like this:
ItemsSource="{Binding XPath=Components/*/????}"
I'm stuck on what to use for ????
The result of this query should display "HealingComponent" I've tried playing around with various different parameters on an online XPath visualizer, but I can't figure this one out. I ready about name(), but I can't seem to get that to work.
Any help would be appreciated
In addition to the ItemsSource you probably need an ItemTemplate, this should work:
<ListBox ItemsSource="{Binding XPath=Components/*}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
If you do not intend to do anything fancy you can also use the DisplayMemberPath, though in this case the binding ensures that Name is not interpreted as an XPath, you may not have that kind of control with DisplayMemberPath.
I am working towards producing a small application that parses XML from a URL and populates a Grid panel based on the contents of the XML. Currently, I have many other elements working properly, but still lack the knowledge needed to hide or show certain columns within the table and having it resize properly. Here's the basic structure of my XAML thus far.
Currently, I feel as though my solution is very poor. I have hard coded each coulmn and row within the Grid and tied their Visibility to a code behind Converter. Under certain conditions, this Converter will return a Visibility of Hidden, but under other conditions it returns the value to display within the table. This feels very sloppy to me, so I assume I've designed this system incorrectly.
My question is more about the proper way to setup this type of system. I am much more familiar with generating the document structure itself within some business logic and then token swapping that generated structure with a token inside the raw document itself. What is a best way to accomplish the goal I'm pursuing?
You could provide the XDocument or XElement retrieved from the web service as the DataContext of an ItemsControl with a Grid. You would then use a DataTemplate to display the information.
XML:
<Entities>
<Person Name="Ted" Age="42" />
<Person Name="Sam" Age="19" />
<Person Name="Bob" Age="25" />
<Person Name="Angie" Age="38" />
</Entities>
Code Behind:
this.DataContext = xdoc;
XAML:
<ItemsControl ItemsSource="{Binding Root.Elements[Person]}"
Grid.IsSharedSizeGroup="True">
<ItemsControl.Resources>
<DataTemplate DataType="Person">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="NameColumn"/>
<ColumnDefinition SharedSizeGroup="AgeColumn" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding Path=Attribute[Name].Value}" />
<TextBox Text="{Binding Path=Attribute[Age].Value}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Linq to XML resources:
WPF Data Binding with LINQ to XML Overview
WPF Data Binding Using LINQ to XML Example
How to: Bind to XDocument, XElement, or LINQ for XML Query Results
I am wondering if anyone has attempted the following or has an idea as to how to do it.
I have a WPFToolkit DataGrid which is bound to an ObservableCollection of items. As such, the DataGrid is shown with as many rows in the ObservableCollection, and as many columns as I have defined in for the DataGrid. That all is good. What I now need is to provide another view of the same data, only, instead, the DataGrid is shown with as many cells in the ObservableCollection.
So let's say, my ObservableCollection has 100 items in it. The original scenario showed the DataGrid with 100 rows and 1 column. In the modified scenario, I need to show it with 10 rows and 10 columns, where each cell shows the value that was in the original representation. In other words, I need to transform my 1D ObservableCollection to a 2D ObservableCollection and display it in the DataGrid. I know how to do that programmatically in the code behind, but can it be done in XAML?
Let me simplify the problem a little, in case anybody can have a crack at this. The XAML below does the following:
* Defines an XmlDataProvider just for dummy data
* Creates a DataGrid with 10 columns
o each column is a DataGridTemplateColumn using the same CellTemplate
* The CellTemplate is a simple TextBlock bound to an XML element
If you run the XAML below, you will find that the DataGrid ends up with 5 rows, one for each book, and 10 columns that have identical content (all showing the book titles). However, what I am trying to accomplish, albeit with a different data set, is that in this case, I would end up with one row, with each book title appearing in a single cell in row 1, occupying cells 0-4, and nothing in cells 5-9. Then, if I added more data and had 12 books in my XML data source, I would get row 1 completely filled (cells covering the first 10 titles) and row 2 would get the first 2 cells filled.
Can my scenario be accomplished primarily in XAML, or should I resign myself to working in the code behind?
Any guidance would greatly be appreciated. Thanks so much!
<UserControl
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:custom="http://schemas.microsoft.com/wpf/2008/toolkit"
mc:Ignorable="d"
x:Name="UserControl"
d:DesignWidth="600" d:DesignHeight="400" >
<UserControl.Resources>
<XmlDataProvider x:Key="InventoryData" XPath="Inventory/Books">
<x:XData>
<Inventory xmlns="">
<Books>
<Book ISBN="0-7356-0562-9" Stock="in" Number="9">
<Title>XML in Action</Title>
<Summary>XML Web Technology</Summary>
</Book>
<Book ISBN="0-7356-1370-2" Stock="in" Number="8">
<Title>Programming Microsoft Windows With C#</Title>
<Summary>C# Programming using the .NET Framework</Summary>
</Book>
<Book ISBN="0-7356-1288-9" Stock="out" Number="7">
<Title>Inside C#</Title>
<Summary>C# Language Programming</Summary>
</Book>
<Book ISBN="0-7356-1377-X" Stock="in" Number="5">
<Title>Introducing Microsoft .NET</Title>
<Summary>Overview of .NET Technology</Summary>
</Book>
<Book ISBN="0-7356-1448-2" Stock="out" Number="4">
<Title>Microsoft C# Language Specifications</Title>
<Summary>The C# language definition</Summary>
</Book>
</Books>
<CDs>
<CD Stock="in" Number="3">
<Title>Classical Collection</Title>
<Summary>Classical Music</Summary>
</CD>
<CD Stock="out" Number="9">
<Title>Jazz Collection</Title>
<Summary>Jazz Music</Summary>
</CD>
</CDs>
</Inventory>
</x:XData>
</XmlDataProvider>
<DataTemplate x:Key="GridCellTemplate">
<TextBlock>
<TextBlock.Text>
<Binding XPath="Title"/>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<custom:DataGrid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsSynchronizedWithCurrentItem="True"
Background="{DynamicResource WindowBackgroundBrush}" HeadersVisibility="All" RowDetailsVisibilityMode="Collapsed"
SelectionUnit="CellOrRowHeader" CanUserResizeRows="False" GridLinesVisibility="None" RowHeaderWidth="35" AutoGenerateColumns="False"
CanUserReorderColumns="False" CanUserSortColumns="False">
<custom:DataGrid.Columns>
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="01" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="02" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="03" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="04" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="05" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="06" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="07" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="08" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="09" />
<custom:DataGridTemplateColumn CellTemplate="{StaticResource GridCellTemplate}" Header="10" />
</custom:DataGrid.Columns>
<custom:DataGrid.ItemsSource>
<Binding Source="{StaticResource InventoryData}" XPath="Book"/>
</custom:DataGrid.ItemsSource>
</custom:DataGrid>
</Grid>
So, to rephrase your question simply, you have a collection of objects and you want to display them horizontally with 10 to a row. If there are more than 10, the extra items wrap around (reflow) to the next row.
Here's how you can do it extremely easily:
Don't use DataGrid. That's basically only for displaying objects 1 object to a row where columns show various pieces of data for that object.
Instead, use ListBox or ListView, and give it a new ItemsPanel, like so (use data templates to display complex objects):
<ListBox ItemsSource="{Binding SomeCollectionOfObjects}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="10"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
This will replace the default VirtualizingStackPanel used by ListBox to arrange its items with UniformGrid. The reason for using UniformGrid is because you can specify the number of Columns and/or Rows the grid should have. The panel will equally divide the display area among the ListBox's items, but with 10 columns (as you want).
If you don't want a fixed number of columns/rows, you could instead use a WrapPanel which would fit as many objects into a single row as possible and wrap to the next row, a bit like inline-block elements in html, or left-justified text.
One last thing: remember that each of these 3 panels I just mentioned can have their Orientation set to either Horizontal (default for UniformGrid and WrapPanel) or Vertical (default for VirtualizingStackPanel and StackPanel).
Furthermore, you could write your own panels for custom item layout (e.g. arranging items in a circle).