I added in my code copying the first TabItem along with all the controls. But now how to reference to these controls to make the appropriate modifications (I mean change label text)? Do I must need make new class and binding ?
I want clone tab item to store data in labels about for example other apps.
XAML code:
<TabControl x:Name="MainTabControl" x:FieldModifier="public">
<TabItem x:Name="TabItem1" x:FieldModifier="public" Header="Tab 1">
<Grid>
<Label x:Name="Label1" Content="Test 1" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" FontSize="18"/>
<Label x:Name="Label2" Content="Test 2" HorizontalAlignment="Left" Margin="11,44,0,0" VerticalAlignment="Top" FontSize="18"/>
<Button x:Name="Button1" Content="Show Open Dialog" IsEnabled="True" HorizontalAlignment="Left" Margin="11,185,0,0" VerticalAlignment="Top" Width="194" Click="Button1_Click"/>
</Grid>
</TabItem>
C# code:
public MainWindow()
{
InitializeComponent();
mainWindow = this;
TabItem tab2 = TrycloneElement(TabItem1);
if (tab2 != null) MainTabControl.Items.Add(tab2);
}
public static T TrycloneElement<T>(T orig)
{
try
{
string s = XamlWriter.Save(orig);
StringReader stringReader = new StringReader(s);
XmlReader xmlReader = XmlTextReader.Create(stringReader, new XmlReaderSettings());
XmlReaderSettings sx = new XmlReaderSettings();
object x = XamlReader.Load(xmlReader);
return (T)x;
}
catch
{
return (T)((object)null);
}
}
You can access the controls by walking the visual tree using Content, Children or other properties.
TabItem tab2 = TrycloneElement(TabItem1);
var grid = (Grid) tab2.Content;
var label1 = (Label) grid.Children[0];
var label2 = (Label) grid.Children[1];
var button = (Button) grid.Children[2];
An alternative is to use the VisualTreeHelper and convenience helper methods like described here:
How can I find WPF controls by name or type?
Now, these methods work, but you do not need them. You are actually trying to reinvent the wheel. Reusing the same visual structure with different data can be achieved with data templating in a convenient way.
Data Templating Overview
You can use the same technique for your TabControl. I will show you a quick conceptual example that would even comply with the MVVM pattern. This example is not complete. It serves as a base for you to learn about an alternative to your current approach. You can of course also make this approach work in code-behind with a few modifications.
Create a data class like below with your application information. Here I assume that it is not editable, otherwise you need to implement INotifyPropertyChanged in order to update the user interface on changes.
public class AppInfo
{
public AppInfo(string name, string description)
{
Name = name;
Description = description;
}
public string Name { get; }
public string Description { get; }
}
Expose a list of AppInfo (or an ObservableCollection if the collection is changed at runtime).
public List<AppInfo> AppInfos { get; }
Initialize the list appropriately and add your items, for example:
AppInfos = new List<AppInfo>
{
new AppInfo("Visual Studio", "A popular IDE."),
new AppInfo("Calculator", "Does the math for you."),
new AppInfo("Chrome", "A web browser.")
};
The tab control would bind its ItemsSource to the AppInfos.
<TabControl ItemsSource="{Binding AppInfos}">
In order to create a reusable visual representation, create a DataTemplate as ItemTemplate (this is the tab header) and a ContentTemplate (this is the tab content).
<TabControl ItemsSource="{Binding AppInfos}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:AppInfo}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type local:AppInfo}">
<Grid>
<Label Content="{Binding Name}"
HorizontalAlignment="Left"
Margin="10,10,0,0"
VerticalAlignment="Top"
FontSize="18"/>
<Label Content="{Binding Description}"
HorizontalAlignment="Left"
Margin="11,44,0,0"
VerticalAlignment="Top"
FontSize="18"/>
<Button Content="Show Open Dialog"
IsEnabled="True"
HorizontalAlignment="Left"
Margin="11,185,0,0"
VerticalAlignment="Top" Width="194"
Command="{Binding DataContext.OpenCommand, RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Now, both templates are reused for each item in the AppInfos collection. The data template uses bindings to the corresponding properties, which automatically pick up the values for each item.
The Button is special here, since it can be clicked to execute an action. This action is probably the same for all items, except that it needs a concrete item to work with. You can encapsulate this logic in commands.
MVVM - Commands, RelayCommands and EventToCommand
This is another important aspect that can make your life easier, but is beyond the scope of this question. There are lots of tutorials out there. You could expose another property:
public ICommand OpenCommand{ get; }
The button in XAML binds its Command to this property (RelativeSource is needed to get the right data context for binding). The CommandParameter will pass the current AppInfo item to the command. In code, you could then execute your custom logic on this item. No need to know about user interface elements, only data.
Disclaimer: I know that this is pretty overwhelming. That is why I provided an immediate (dirty) solution to your issue and an example with links to resources to gradually learn a better way with concrete samples for your exact problem. Once you get used to MVVM, data binding and data templating, you will see how much easier more complex issues can be solved and what the benefits are in terms of maintainability and reuseability.
Related
I'm developing a WPF app using MVVM pattern with Caliburn.Micro
I have a config file that contains positions of where XAML elements should be inside of a StackPanel
# in this case RU_ELEMENT should be at the top, EN_ELEMENT second and DE_ELEMENT last
EN_ELEMENT = 1
DE_ELEMENT = 2
RU_ELEMENT = 0
This seems to be pretty basic yet I'm unable to find a way to do this. I found this thread: change the children index of stackpanel in wpf but changing it this way seems to be too complicated for what I am after. I just need to set an index of an element from a variable. I feel like there should be a much simpler way. I'm also ok with using some other, perhaps more appropriate layout panel than StackPanel.
XAML:
<!-- Language1 -->
<TextBlock Text="English" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding SelectedItem.ValueEN, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox('english')]" />
<!-- Language2 -->
<TextBlock Text="German" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding SelectedItem.ValueDE, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox('german')]" />
On a side note: I find WPF and C# in general to have much less discussions and "how to" guides than all of my previous languages (Java, Python, JS) so researching things online is usually a dead end for me. I'm not sure to why that is since C# is a very popular language but I'm really struggling with finding help online.
A solution could be to use an ItemsControl that would host the xaml elements. You can bind the items like <ItemsControl ItemsSource="{Binding ListOfItems} ...
Then you could easily sort the items in the corresponding ViewModel. Like so:
public BindableCollection<YourElement> ListOfItems {get;set;}
...
ListOfItems.Sort()
Note that YourElement class should have a comparator.
EDIT: As per request I'll explain it more detailed:
In your Xaml you have to declare a ItemsControl like so:
<ItemsControl ItemsSource="{Binding ListOfItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Language}" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding TextValue, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox($this)]" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And in your backend you should first create a class that's going to represent your item in the ItemsControl. For example:
public Class MyItem{
public string Language {get;set;}
public string TextValue {get;set;}
}
Finally in your ViewModel you'll need to create the list of items that you bind with the ItemsControl like so:
public BindableCollection<MyItem> ListOfItems {get;set;}= new BindableCollection<MyItem>();
//here you can add them in the order that is specified by the config file
public void LoadItems(){
ListOfItems.Add(new MyItem{Language="English"});
ListOfItems.Add(new MyItem{Language="Russian"});
ListOfItems.Add(new MyItem{Language="German"});
}
public void FocusedTextBox(MyItem item){
//do here whatever you want
}
Im having difficulty displaying a variable within a text box.
Mainly trying to understand the way that Bindings work in XAML.
I need to try to display the variable TextBoxFileName and TextBoxFilePath in the relevant fields. The information the variables are retrieving is stored within a seperate GlobalVariableStorage class. I do not want the TextBox fields to be editable so ive set them as read only. I do not want the user to be able to edit the data in these fields at all. If you have ideas for alternative display methods, please feel free to suggest.
XAML
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<!-- Placeholder for the theme dictionary -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Page.Resources>
<Frame Background="{StaticResource CustomAcrylicDarkBackground}">
<StackPanel>
<TextBox Width="500" Header="File Name" PlaceholderText="Name Of File" IsReadOnly="True" Foreground="White" Text=""/>
<TextBox Width="500" Header="File Location" PlaceholderText="File Location" IsReadOnly="True" Foreground="White" Text=""/>
</StackPanel>
</Frame>
CODE BEHIND
public sealed partial class SettingsPage : Page
{
public SettingsPage()
{
this.InitializeComponent();
}
public class TextBoxDisplay
{
public string TextBoxFileName = GlobalVariables.FileName;
public string TextBoxFilePath = GlobalVariables.FilePath;
}
}
You are having a difficult time because you have a wrong approach.
For instance, you didn't follow the MVVM pattern. Plus, you don't need to set the IsReadOnly to true, you just use One-Way Binding.
<TextBox Text="{Binding TextBoxFileName,Mode=OneWay}"/>
To understand and implement MVVM correctly, I suggest you to read the following links: MVVM for WPF. Although it is for WPF, but UWP is very similar to WPF, you won't get any problem.
If you want to learn MVVM, I can help you. Just send me a message on Discord: Red Wei#2396
Have fun.
Add two read-only properties to your SettingsPage class:
public sealed partial class SettingsPage : Page
{
public SettingsPage()
{
this.InitializeComponent();
}
public string TextBoxFileName => GlobalVariables.FileName;
public string TextBoxFilePath => GlobalVariables.FilePath;
}
...and bind to these:
<TextBox Header="File Name" ... Text="{x:Bind TextBoxFileName}"/>
<TextBox Header="File Name" ... Text="{x:Bind TextBoxFilePath}"/>
I am trying to make a form of sorts where the user will have the option to press a button to dynamically add more text boxes, with each text box to contain the path of a directory to exclude from a search. This is relatively trivial using code-behind, but my problem is doing it in proper MVVM.
The general markup for the structure is:
<ScrollViewer >
<StackPanel>
<DockPanel LastChildFill="False">
<TextBox DockPanel.Dock="Left"/>
<Button DockPanel.Dock="Right"
Content="+"/>
</DockPanel>
</StackPanel>
</ScrollViewer>
Clicking the button will add a new DockPanel with a TextBox and Button to the StackPanel. All buttons but the bottom-most will change to a minus sign. How can I somehow bind to the text of all the text boxes?
As an aside, once/if I get this working, would it be better to make it into its own component?
Quick pseudo-code to get you started:
cs (view model):
// INotifyPropertyChanged if you need, as well as full-properties with notification
public class Item
{
public string Text {get; set;}
}
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
void OnCommandAdd() => Items.Add(new Item());
xaml (view):
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The idea is to use control able to display list (e.g. ItemsControl) and collection of items in view model. Adding new element to view is done by adding item to that collection.
All TextBoxes will have Text bound to corresponding property of item.
So, I made a really simple attempt to try out data binding from a property of a class that I have, but, for whatever reason, the code actually do anything. It's not throwing any errors, but something must not be working right. I'm just currently testing if it'll behave like I want it to, which, in this case, will set the opacity of a rectangle to zero. Here's the xaml for the Data Template that doesn't seem to want to respond correctly:
<HubSection x:Name="China" Width="440" Height="460" Background="#FF343434" Header="China" IsHeaderInteractive="True" Tapped="{x:Bind HubSectionTapped}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="50,0,0,0">
<DataTemplate x:DataType="data:MainPageView">
<Grid Height="460" Width="410" VerticalAlignment="Bottom" x:Name="ChinaBackground">
<Image Source="Assets/chinaFlag.bmp" x:Name="ChinaFlag"/>
<Rectangle x:Name="ChinaSelected_Rect" Width="410" Height="30" VerticalAlignment="Bottom" Fill="BlueViolet" Opacity="{x:Bind Opacity1}"/>
</Grid>
</DataTemplate>
</HubSection>
And here's the code behind:
public MainPageView TheMainPageView;
public MainPage()
{
this.InitializeComponent();
timer = new DispatcherTimer();
timer.Tick += Timer_DyanmicResize;
timer.Tick += Timer_SelectionIndicator;
timer.Start();
TheMainPageView = new MainPageView ();
}
And finally, here's the class MainPageView that's referenced:
public class MainPageView
{
public int Opacity1 {get; set;}
public int Opacity2 {get;set;}
public int Opacity3 { get; set; }
public MainPageView()
{
this.Opacity1 = 0;
this.Opacity2 = 0;
this.Opacity3 = 0;
}
}
In the XAML I included the xmlns:data="using:TestApp.Models" (models is the folder in which the class MainPageView is housed). As I said, it's not throwing errors, but it's not doing anything either, so I'm a bit at a loss of where to start addressing this because there aren't any errors to trace back. Thanks in advance for any help you guys can provide
HubSection uses a DataTemplate to define the content for the section, content can be defined inline, or bound to a data source. When using binding in this DataTemplate, we need set DataContext property of HubSection to provide data source for the DataTemplate.
{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself. So it will look in the code-behind of your page or user control for properties, fields, and methods.
This is right when you use {x:Bind} directly in page or user control. While Inside a DataTemplate, there is a little difference.
Inside a DataTemplate (whether used as an item template, a content template, or a header template), the value of Path is not interpreted in the context of the page, but in the context of the data object being templated. So that its bindings can be validated (and efficient code generated for them) at compile-time, a DataTemplate needs to declare the type of its data object using x:DataType.
For more information about Data binding in UWP, please check Data binding in depth.
To fix your issue, you just need to set DataContext in HubSection like following:
<HubSection x:Name="China" Width="440" Height="460" Background="#FF343434" Header="China" IsHeaderInteractive="True" Tapped="{x:Bind HubSectionTapped}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="50,0,0,0" DataContext="{x:Bind TheMainPageView}">
<DataTemplate x:DataType="data:MainPageView">
<Grid Height="460" Width="410" VerticalAlignment="Bottom" x:Name="ChinaBackground">
<Image Source="Assets/chinaFlag.bmp" x:Name="ChinaFlag"/>
<Rectangle x:Name="ChinaSelected_Rect" Width="410" Height="30" VerticalAlignment="Bottom" Fill="BlueViolet" Opacity="{x:Bind Opacity1}"/>
</Grid>
</DataTemplate>
</HubSection>
Here when using {x:Bind} in HubSection, it uses the page itself as its data source as HubSection is in the page directly. So it can get TheMainPageView field in the code-behind. But for the {x:Bind} in DataTemplate, it can't as
its data source is the data object being templated not the page. So we need to provide this data object by setting DataContext property of HubSection.
Check you output window for errors but I imagine you might see a binding error in there. Opacity is a double, you are using an int so will get a type conversion error.
I have an existing ViewModel and View in an MVVM project. Effectively this View presents a collection of items in a particular, styled way. I'll call this existing ViewModel "CollectionPresenter".
Up to now, this has been presented as as follows in XAML:
<Grid>
<ns:CollectionPresenter />
</Grid>
Now, I want to have a dynamic collection of these "CollectionPresenter" view models made available ideally in a tab view.
My approach has been to define an observable collection of these "CollectionPresenters", creating them first on construction of the parent view model. The XAML above then changed to look something like this:
<TabControl ItemsSource="{TemplateBinding CollectionPresenters}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding CollectionPresenterTitle}">
</DataTemplate>
<TabControl.ItemTemplate>
<TabControl.ContentTemplate>
... this is where things get confusing
</TabControl.ContentTemplate>
<TabControl>
You can see above my problem is the ContentTemplate.
When I load this up, I get a tab control and it has as many tabs as my observable collection of "CollectionPresenter" objects.
However, the content of the tab control is always empty.
Is this approach correct - and is there a better way regardless?
EDIT: ADDING SOME EXTRA THINGS TO MAKE IT CLEARER
I've tried the below, but it doesn't work. The XAML with the Tab Control (the binding to "Things" works fine):
<TabControl ItemsSource="{TemplateBinding Things}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:Thing}">
<TextBlock Text="{Binding ThingName}" Width="200" Background="Blue" Foreground="White"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type viewModels:Thing}">
<TextBlock Text="{Binding ThingName}" Width="500" Height="500" Background="Blue" Foreground="White"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The definition for the "Things" observable collection (which is inside the templated parent (ParentObject) of the XAML with the tab control):
public static readonly DependencyProperty ThingsProperty =
DependencyProperty.Register("Things", typeof(ObservableCollection<Thing>), typeof(ParentObject), new PropertyMetadata(null));
public ObservableCollection<Thing> Things
{
get { return (ObservableCollection<Thing>)GetValue(ThingsProperty); }
set { SetValue(ThingsProperty, value); }
}
Stripped down version of the "Thing" view model:
public class Thing : ViewModelBase
{
public Thing()
{
}
public void Initialise(ObservableCollection<Thing> things, string thingName)
{
Things = things;
ThingName = thingName;
}
public static readonly DependencyProperty ThingNameProperty =
DependencyProperty.Register("ThingName", typeof(string), typeof(Thing), new PropertyMetadata(null));
public string ThingName
{
get { return (string)GetValue(ThingNameProperty); }
set { SetValue(ThingNameProperty, value); }
}
}
Looking at my answer to the WPF MVVM navigate views question, you can see this:
<DataTemplate DataType="{x:Type ViewModels:MainViewModel}">
<Views:MainView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:PersonViewModel}">
<Views:PersonView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:CompanyViewModel}">
<Views:CompanyView />
</DataTemplate>
Now, wherever we use an instance from one of these types in our application, these DataTemplates will tell the framework to display the related view instead.
Therefore, your solution is to simply not hard-code one single DataTemplate to the TabControl.ItemTemplate property, but to leave that blank instead. If you use multiple DataTemplates without providing x:Key values, then they will implicitly be applied when each data object is to be rendered in the TabControl.
UPDATE >>>
Using these DataTemplates should leave your TabControl looking like this:
<TabControl ItemsSource="{TemplateBinding Things}" />
I'm not sure why you're using a TemplateBinding there though as you don't need to define any new templates to get this working... therefore, you should be using a plain old Binding instead.
One other thing that you need to do is to use different data types for each item in the collection that you want to display differently. You could derive custom classes from your Thing class and so the collection could still be of type ObservableCollection<Thing>.