WinRT Selectively hide a child of a ListViewItem based on the ListViewItem's sibling - winrt-xaml

I am using a ListView and set the ItemsSources to an ObservableCollection of Message objects. The DataTemplate for the ListViewItem contains a custom control. I have defined a Message property on the custom control and am binding the Message property on this custom control to the dataContext of the ListViewItem. When the loaded event gets fired on the custom control, I want to compare the ReceivedDate set on its Message to the ReceivedDate set on the sibling of this ListViewItem. This is not working consistently since the Message property is set on some of the ListViewItems by the time the Loaded event is fired and not set on some.
Example:
<local:ListView x:Name="MyMessagesView"
Visibility="Collapsed"
ItemsSource="{Binding Messages}"
ItemContainerStyle="{StaticResource AppListViewItemStyle}">
<ListView.ItemTemplate>
<DataTemplate>
<dm:MyScreenRow Message="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</local:ListView>
In the code behind of my CustomControl, I handle the Loaded event. I want to compare the ReceivedTime property set on the current ListViewItem to its previous sibling and show a child element in the ListViewItem. Is Loaded event not the correct place to handle this? If not, any recommendations on how to handle this?
public MyScreenRow()
{
this.InitializeComponent();
this.Loaded += MyScreenRow_Loaded;
}
private static void MyScreenRow_Loaded(object sender, RoutedEventArgs e)
{
MyScreenRow screenRow = sender as MyScreenRow;
if (screenRow.Message == null)
{
return;
}
ListViewItemPresenter presenter = (ListViewItemPresenter)VisualTreeHelper.GetParent(screenRow);
if (presenter != null)
{
ListViewItem currentViewItem = (ListViewItem)VisualTreeHelper.GetParent(presenter);
if (currentViewItem != null)
{
ListView listView = (ListView)ItemsControl.ItemsControlFromItemContainer(currentViewItem);
if (listView != null)
{
int indexOfCurrentContainer = listView.IndexFromContainer(currentViewItem);
if (indexOfCurrentContainer == 0)
{
screenRow.GroupHeader.Visibility = Visibility.Visible;
}
else
{
// Get the previous container info
ListViewItem previousViewItem = (ListViewItem)listView.ContainerFromIndex(indexOfCurrentContainer - 1);
if (previousViewItem != null)
{
MyScreenRow previousScreenRow = (MyScreenRow)previousViewItem.ContentTemplateRoot;
if (previousScreenRow != null
&& previousScreenRow.Message != null
&& previousScreenRow.Message.ReceivedTimeUtc != null)
{
if (screenRow.Message != null
&& screenRow.Message.ReceivedTimeUtc != null)
{
if (previousScreenRow.Message.ReceivedTimeUtc.Date.CompareTo(screenRow.Message.ReceivedTimeUtc.Date) != 0)
{
screenRow.GroupHeader.Visibility = Visibility.Visible;
}
}
}
}
}
}
}
}
}

Yes.. as you guessed doing it in the loaded event will not work becuase the loaded event is fired only Once when the listview is first instantiated and loaded with some elements after that it isn't fired.
I strongly suggest you use the LayoutUpdated Event .
This is a part of UIElement so every control in the windows universe must have it.
it is triggered when there is a layout change .. in case of lists when new items are added or deleted ..
Now to some more fun part .. if you don't want to do it all your controls at once then you can use ContainerContentChanging this will allow you to apply changes on the items in view only.
<ListView LayoutUpdated="ListView_LayoutUpdated" ContainerContentChanging="ListView_ContainerContentChanging"/>
Quick tutorial link : http://blogs.msdn.com/b/hanxia/archive/2013/11/04/incremental-update-item-data-for-listviewbased-controls-in-windows-8-1.aspx

Related

Acumatica - FieldDefaulting update ImageUrl from DAC extension

I am trying to update the Inventory Item ImageUrl if it is found to be null with some conditions. I have added a Usr field called UsrStyleImg to the Item Class screen. This field is for a basic image of an item and it is stored in the database. The functionality I want is if the Inventory Item does not have an image in the ImageUrl then it will default to the UsrStyleImg that is connected with the ItemClassID. ItemClassID is also found on the Stock Item Screen. Here is the code I have in the InventoryItemMaint graph:
protected void InventoryItem_ImageUrl_FieldDefaulting(PXCache cache, PXFieldDefaultingEventArgs e)
{
var row = (InventoryItem)e.Row;
if (row == null) return;
var item = (INItemClass)PXSelect<INItemClass, Where<INItemClass.itemClassID, Equal<Current<InventoryItem.itemClassID>>>>.Select(Base, row.ItemClassID);
var image = PXSelect<InventoryItem, Where<InventoryItem.imageUrl, Equal<Current<InventoryItem.imageUrl>>>>.Select(Base, row.ImageUrl);
if (image != null)
return;
else {
e.NewValue = item.GetExtension<INItemClassExt>().UsrStyleImg;
}
}
The code compiles fine but when I test with an Item that has an Item Class attached to it with an image in the INItemClass table called UsrStyleImg it does not populate to the imageUrl found in the Inventory Item table or the Stock Item screen. I have also tried this with FieldSelecting and using the e.ReturnValue with still the same results.
If I need more clarification please let me know.
Try using a RowSelecting Event
protected virtual void InventoryItem_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
InventoryItem row = e.Row as InventoryItem;
//Extra checks to prevent infinite loops
if (row != null && !string.IsNullOrWhiteSpace(row.InventoryCD) && Base.Item.Cache.GetStatus(row) == PXEntryStatus.Notchanged)
{
if (!string.IsNullOrWhiteSpace(row.ItemClassID))
{
//You must always use a PXConnectionScope if Selecting during RowSelecting
using (new PXConnectionScope())
{
//If you're going to pass in a value in .Select, use Required instead of Current.
INItemClass itemClass = PXSelectReadonly<INItemClass, Where<INItemClass.itemClassID, Equal<Required<INItemClass.itemClassID>>>>.Select(Base, row.ItemClassID);
if (itemClass != null && string.IsNullOrWhiteSpace(row.ImageUrl))
{
INItemClassExt itemClassExt = itemClass.GetExtension<INItemClassExt>();
//To prevent unneeded update if it's blank
if (!string.IsNullOrWhiteSpace(itemClassExt.UsrStyleImg))
{
row.ImageUrl = itemClassExt .UsrStyleImg;
//Force set the status in the Cache, otherwise it infinite loops
Base.Item.Cache.SetStatus(row, PXEntryStatus.Updated);
Base.Item.Update(row);
}
}
}
}
}
}

Popup exceptions in line items (SOLine)

How to display popup exceptions instead of the one with a red dot on the row for the line items? I was able to have them in RowInserting for a little bit but after a series of code changes I get the red dot one instead. It even did not create a new row as I wanted but now it does. RowInserting event:
protected virtual void SOLine_RowInserting(PXCache sender, PXRowInsertingEventArgs e)
{
var listMissingOEdesc = new List<int>();
var select = Base.Transactions.Select();
foreach (SOLine row in select)
{
var isOEDesc = IsOEDescEnabled(sender, row);
var rowExt = PXCache<SOLine>.GetExtension<SOLineExt>(row);
if (isOEDesc == true)
if (rowExt.UsrOedesc == null)
listMissingOEdesc.Add(row.SortOrder.Value);
}
if (listMissingOEdesc.Count > 0)
{
throw new PXException("Line items with sort order {0} do not have OE Desc filled out. Cannot add a new line.", string.Join(", ", listMissingOEdesc));
}
else
Base.Actions.PressSave();
}
Thanks!
There is no easy way, if any at all, to display popup exception and prevent the grid from inserting a new row. The way the framework is designed, PXGrid first inserts a new row on the webpage. After that PXGrid sends a callback to the application server either requesting default field values for the new row (if PXGrid's Mode has InitNewRow="True") or sends all values captured from the webpage to the application server, so the new row can be inserted in the PXCache. Whenever an event handler, on the field or row level, gets invoked, the new row will still be visible to the user on the web page. Even if you invoke Ask method on the Transactions data view within one of the event handlers, the new row won't disappear from the webpage.
With all that said, the best and probably the only way to display popup exception and prevent the grid from inserting a new row is by replacing the standard Add New Row button on PXGrid with a custom action, which will first run the validation and display popup exception, if necessary. Otherwise, a new row will be inserted into PXGrid. It's also required to enable or disable the custom NewSOTran action based on the state of the standard PXGrid's Insert button.
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public void SOOrder_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
NewSOTran.SetEnabled(Base.Transactions.AllowInsert);
}
public PXAction<SOOrder> NewSOTran;
[PXButton(CommitChanges = true)]
[PXUIField]
protected void newSOTran()
{
var listMissingOEdesc = new List<SOLine>();
var select = Base.Transactions.Select();
foreach (SOLine row in select)
{
var isOEDesc = !string.IsNullOrEmpty(row.TranDesc);
if (isOEDesc == true)
listMissingOEdesc.Add(row);
}
if (listMissingOEdesc.Count > 0)
{
throw new PXException("Cannot add a new line.");
}
var newTran = Base.Transactions.Insert();
Base.Transactions.Cache.ActiveRow = newTran;
}
}
In Aspx there will required 3 major chages:
SyncPositionWithGraph property set to True for PXGrid:
<px:PXGrid ID="grid" runat="server" DataSourceID="ds" Width="100%" TabIndex="100"
SkinID="DetailsInTab" StatusField="Availability" SyncPosition="True" Height="473px"
SyncPositionWithGraph="True" >
the standard AddNew action must be replaced by a custom NewSOTran action:
<ActionBar>
<Actions>
<AddNew ToolBarVisible ="False" />
</Actions>
<CustomItems>
<px:PXToolBarButton CommandName="NewSOTran" CommandSourceID="ds"
DisplayStyle="Image">
<Images Normal="main#AddNew" />
<ActionBar GroupIndex="0" Order="2" />
</px:PXToolBarButton>
...
</CustomItems>
</ActionBar>
AllowAddNew property set to false in the Mode section of PXGrid to prevent the standart Insert button from execution when users use keyboard shortcuts or double-click on PXGrid:
<Mode InitNewRow = "True" AllowFormEdit="True" AllowUpload="True" AllowDragRows="true"
AllowAddNew="false" />
To select new records’ first cell and switch it to the edit mode (unless the first cell is read-only), it's also required to subscribe to the Initialize and ToolsButtonClick client events of PXGrid:
<ClientEvents Initialize="initTransactionsGrid"
ToolsButtonClick="transactionsGriduttonClick" />
and define the following JavaScript functions:
var isInitEvents = false;
function initTransactionsGrid(a, b) {
if (isInitEvents) return;
isInitEvents = true;
a.events.addEventHandler("afterRepaint", editNewSOTran);
}
function editNewSOTran() {
if (lastActiveRowIndex != null) {
var grid = px_alls["grid"];
if (grid.activeRow) {
var activeRowIndex = grid.rows.indexOf(grid.activeRow);
if (activeRowIndex != lastActiveRowIndex) {
grid.activeRow.activateCell(0, false);
grid.beginEdit();
}
}
lastActiveRowIndex = null;
}
}
var lastActiveRowIndex;
function transactionsGriduttonClick(sender, args) {
if (args.button && args.button.commandName == "NewSOTran") {
if (sender.activeRow)
lastActiveRowIndex = sender.rows.indexOf(sender.activeRow);
else
lastActiveRowIndex = -1;
return;
}
lastActiveRowIndex = null;
}
To package custom JavaScript code into customization, in Layout Editor you can drag and drop a Java Script element from the Add Controls tab and copy your entire JavaScript code into the Script property.

How to stop onItemSelected() from firing off multiple times after a selection of an item was made?

I've seen another similar thread, but I wasn't able to resolve my issue with the given answers.
EXPLANATION OF MY GOALS:
I have 4 spinners, each has its own ArrayList of strings assigned to it via an adapter. All of these arrays contain the same values at the beginning.
I want to remove the selected value (eg. "item" in spinner1) from all the other spinners (remove "item" from spinner2, 3 and 4) when it is selected.
PROBLEM:
When I select an item for the first two or three times from different spinners (the number of selections needed to reproduce the problem varies) the onItemSelected() method gets called multiple times (the number of callings is greater than the number of actual -user- selections made).
QUESTION:
How to prevent the calling of onItemSelected(); at unnecessary times. I want it to be called only when the actual user makes a selection in one of the spinners and only call it once when that does happen.
If you want to try to help me out and you need more code / images of the problem on the device itself, please, say so.
Here is my whole onItemSelected() method:
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (inCorrection == false)
{
s1 = spinner1.getSelectedItemPosition();
s2 = spinner2.getSelectedItemPosition();
s3 = spinner3.getSelectedItemPosition();
s4 = spinner4.getSelectedItemPosition();
testcount++;
switch(parent.getId())
{
case R.id.v1_q1_s1:
if((position != AdapterView.INVALID_POSITION) && (spinner1.getSelectedItem().toString() != "Default---"))
{
findLists(myList2, myList3, myList4, spinner1.getSelectedItem().toString());
if(returnChecks(0) != "Default---")
{
myList2.add(returnChecks(0));
myList3.add(returnChecks(0));
myList4.add(returnChecks(0));
}
addChecks(0, (spinner1.getSelectedItem().toString()));
}
else
{
if(position != AdapterView.INVALID_POSITION)
{
myList2.add(returnChecks(0));
myList3.add(returnChecks(0));
myList4.add(returnChecks(0));
addChecks(0, (spinner1.getSelectedItem().toString()));
}
}
adapter1.notifyDataSetChanged();
adapter2.notifyDataSetChanged();
adapter3.notifyDataSetChanged();
adapter4.notifyDataSetChanged();
Toast.makeText(Vprasalnik1.this, myList1.toString()+"\n"+myList2.toString()+"\n"+myList3.toString()+"\n"+myList4.toString()+"\n"+checks.toString(), Toast.LENGTH_LONG).show();
break;
case R.id.v1_q1_s2:
if((position != AdapterView.INVALID_POSITION) && (spinner2.getSelectedItem().toString() != "Default---"))
{
findLists(myList1, myList3, myList4, spinner2.getSelectedItem().toString());
if(returnChecks(1) != "Default---")
{
myList1.add(returnChecks(1));
myList3.add(returnChecks(1));
myList4.add(returnChecks(1));
}
addChecks(1, (spinner2.getSelectedItem().toString()));
}
else
{
if(position != AdapterView.INVALID_POSITION)
{
myList1.add(returnChecks(1));
myList3.add(returnChecks(1));
myList4.add(returnChecks(1));
addChecks(1, (spinner2.getSelectedItem().toString()));
}
}
adapter1.notifyDataSetChanged();
adapter2.notifyDataSetChanged();
adapter3.notifyDataSetChanged();
adapter4.notifyDataSetChanged();
Toast.makeText(Vprasalnik1.this, myList1.toString()+"\n"+myList2.toString()+"\n"+myList3.toString()+"\n"+myList4.toString()+"\n"+checks.toString(), Toast.LENGTH_LONG).show();
break;
case R.id.v1_q1_s3:
if((position != AdapterView.INVALID_POSITION) && (spinner3.getSelectedItem().toString() != "Default---"))
{
findLists(myList2, myList1, myList4, spinner3.getSelectedItem().toString());
if(returnChecks(2) != "Default---")
{
myList2.add(returnChecks(2));
myList1.add(returnChecks(2));
myList4.add(returnChecks(2));
Toast.makeText(Vprasalnik1.this, "before: "+returnChecks(2), Toast.LENGTH_LONG).show();
}
addChecks(2, (spinner3.getSelectedItem().toString()));
Toast.makeText(Vprasalnik1.this, "after: "+returnChecks(2), Toast.LENGTH_LONG).show();
}
else
{
if(position != AdapterView.INVALID_POSITION)
{
myList2.add(returnChecks(2));
myList1.add(returnChecks(2));
myList4.add(returnChecks(2));
addChecks(2, (spinner3.getSelectedItem().toString()));
}
}
adapter1.notifyDataSetChanged();
adapter2.notifyDataSetChanged();
adapter3.notifyDataSetChanged();
adapter4.notifyDataSetChanged();
Toast.makeText(Vprasalnik1.this, myList1.toString()+"\n"+myList2.toString()+"\n"+myList3.toString()+"\n"+myList4.toString()+"\n"+checks.toString(), Toast.LENGTH_LONG).show();
break;
case R.id.v1_q1_s4:
if((position != AdapterView.INVALID_POSITION) && (spinner4.getSelectedItem().toString() != "Default---"))
{
findLists(myList2, myList3, myList1, spinner4.getSelectedItem().toString());
if(returnChecks(3) != "Default---")
{
myList2.add(returnChecks(3));
myList3.add(returnChecks(3));
myList1.add(returnChecks(3));
}
addChecks(3, (spinner4.getSelectedItem().toString()));
}
else
{
if(position != AdapterView.INVALID_POSITION)
{
myList2.add(returnChecks(3));
myList3.add(returnChecks(3));
myList1.add(returnChecks(3));
addChecks(3, (spinner4.getSelectedItem().toString()));
}
}
adapter1.notifyDataSetChanged();
adapter2.notifyDataSetChanged();
adapter3.notifyDataSetChanged();
adapter4.notifyDataSetChanged();
Toast.makeText(Vprasalnik1.this, myList1.toString()+"\n"+myList2.toString()+"\n"+myList3.toString()+"\n"+myList4.toString()+"\n"+checks.toString(), Toast.LENGTH_LONG).show();
break;
}
correctSelection();
}
}
At the end of the above code there is a function I call named correctSelection();, that corrects the selection of all spinners, because it doesn't work correctly otherwise - it looks like this:
void correctSelection()
{
inCorrection = true;
spinner1.setSelection(myList1.lastIndexOf(returnChecks(0)));
spinner2.setSelection(myList2.lastIndexOf(returnChecks(1)));
spinner3.setSelection(myList3.lastIndexOf(returnChecks(2)));
spinner4.setSelection(myList4.lastIndexOf(returnChecks(3)));
inCorrection = false;
}
/*it sets the position of all spinners to the last "saved"
(current) item selected, so it corrects the possible index offset that occurs otherwise
(returnChecks(); returns the last item selected from an array in a string format)
PS: To avoid the calling of onItemSelected() in case of programmatically setting the selection
of spinners, I've input a boolean flag (variable "inCorrection"), which is set to false before the
selections are made by "the application" and then set back to false when the code gets run.
*/
To prevent onItemSelected() from being called when you set up the spinner, you can do it like this:
spinner.setOnItemSelectedListener(null);
adapter.notifyDatasetChanged();
spinner.setSelection(0, false);
spinner.setOnItemSelectedListener(onItemSelectedListener);
Explanation:
The framework fires the onItemSelected event when a change in the selection has occurred. It detects a change by registering the current selected position and the previous selected position (mSelectedPostion and mOldSelectedPosition).
When you call notifyDatasetChanged the framework performs various checks to see if the previous selection can be found, so onItemSelected may or may not be called when the spinner is laid out.
By calling setSelection(0, false) these positions are set to 0, possibly detecting a change, but since onItemSelectedListener is null, onItemSelected wont be fired. Position 0 is selected because I guess the "Default---" value is the first position in the list. You can choose another position if you like.
When the spinner is later laid out there is no change, so onItemSelected wont be fired here either.
Note that this has been established by debugging on API level 19 (Android 4.4 KitKat). I don't know if it works on other versions, and I haven't been able to find anything in the documentation to support it.
You can stop the spinner from firing prior to the user making a selection via the optional animation field in the setSelection method. Be sure to order your code this way:
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, yourList);
spinner.setAdapter(spinnerAdapter);
spinner.setSelection(0, false); //stops spinner from firing prior to user selection
as explained by user1801374 , I made the fix for my case. Just make sure before and after selection index remains same in order to not to invoke the onItemSelected again.
private int spinnerIndex = 0;
spinner.setSelection(spinnerIndex, false);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{ public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
// Your code here
//I was detaching and reattaching one fragment which was calling the onItemSelected multiple times, recursively.
spinnerIndex = i;
spinner.setSelection(spinnerIndex, false);
return;
}
public void onNothingSelected(AdapterView<?> adapterView) {
// Your code here
return;
}
}

Connect marker with EditPart

I have a graphical editor which extends GraphicalEditorWithFlyoutPalette.
There could be appear different markers, so it would be nice, if there is any possibility to connect the marker with the EditPart.
I think one possibility is to extend the TableViewer and the corresponding cell classes. But perhaps there is a better and more easier way.
I create my test markers like following:
IResource resource = (IResource) input.getAdapter(IResource.class);
try
{
IMarker marker = resource.createMarker(IMarker.PROBLEM);
marker.setAttribute(IMarker.TEXT, "text");
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
marker.setAttribute(IMarker.MESSAGE, "message");
}
catch (CoreException e)
{
e.printStackTrace();
}
input is my IEditorInput.
In my first attempt, I was trying to extends the ExtendedMarkersView, which fails because it is an internal class.
Another way was to write the view and all corresponding stuff new, but it seems to be senseless.
So I found a work around based on https://stackoverflow.com/a/10501971/390177.
While creating the IMarker, I set additional attributes to link the corresponding data object. With the help of the object I can search for the AbstractGraphicalEditPart with the EditPartRegistry.
After that it is possible to create a selection on the EditPart and reveal to it.
#Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
IStructuredSelection s = (IStructuredSelection) selection;
if (s.getFirstElement() instanceof MarkerItem) {
MarkerItem marker = (MarkerItem) s.getFirstElement();
if (marker != null && marker.getMarker() != null) {
IMarker iMarker = marker.getMarker();
AbstractGraphicalEditPart editPart = null;
DataObject object ...
editPart = (AbstractGraphicalEditPart) getGraphicalViewer().getEditPartRegistry().get(object);
if (editPart != null) {
StructuredSelection eSelection = new StructuredSelection(editPart);
getGraphicalViewer().setSelection(eSelection);
// once selected if you want to get it so the
// graphicalviewer scrolls to reveal the part on the
// screen
getGraphicalViewer().reveal(editPart);
}
}
} else {
super.selectionChanged(part, selection);
}
}

Inconsistent display behavior for Quick Launch menu in MOSS 2007

I'm trying to configure the Quick Launch menu to only display the ancestors and descendant nodes of the currently select node. The menu also needs to display all the childern of the root node. More simply:
Given a site map of:
RootSite
---SubSite1 = navigation set at "Display the current site, the navigation items below the current site, and the current site's siblings"
-----Heading1 = navigation set at "Display the same navigation items as the parent site"
-------Page1 = navigation set at "Display the same navigation items as the parent site"
-------Page2 = navigation set at "Display the same navigation items as the parent site"
-----Heading2 = navigation set at "Display the same navigation items as the parent site"
---SubSite2 = navigation set at "Display the current site, the navigation items below the current site, and the current site's siblings"
-----Heading1 = navigation set at "Display the same navigation items as the parent site"
SiteMapProvider configuration:
<PublishingNavigation:PortalSiteMapDataSource ID="SiteMapDS" Runat="server"
SiteMapProvider="CurrentNavSiteMapProvider" EnableViewState="true"
StartFromCurrentNode="true" ShowStartingNode="false"/>
The expected and actual behavior of the Quick Launch menu displayed at SubSite1 is:
---SubSite1
-----Heading1
-------Page1
-------Page2
-----Heading2
---SubSite2
The expected behavior of the menu after navigating to Heading1 of SubSite2:
---SubSite1
---SubSite2
-----Heading1
What I actually see after navigating to Heading1 of SubSite2:
---SubSite1
-----Heading1
-------Page1
-------Page2
-----Heading2
---SubSite2
-----Heading1
This does not match what I expect to see if I set the Heading1 navigation to "Display the
same navigation items as the parent site" and SubSite2 is set to "Display the current site, the navigation items below the current site, and the current site's siblings". I expect
Heading1 to inherit the navigation item of SubSite2 with the SubSite1 items collapsed from view. I've also played with the various
Trim... attributes without success. Any help will be greatly appreciated!
I followed #Nat's guidance into the murky world Sharepoint webparts to achieve the behavior I described above. My approach was to roll my own version of the MossMenu webpart that Microsoft has released through the ECM Team Blog. This code is based on the native AspMenu control. I used this control to "intercept" the native SiteMapDataSource injected into through DataSourceId attribute in the markup and create a new XML data source to exhibit the desired behavior. I've included the final source code at the end of this wordy answer. Here are the bits from the master page markup:
<%# Register TagPrefix="myCustom" Namespace="YourCompany.CustomWebParts"
Assembly="YourCompany.CustomWebParts, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=9f4da00116c38ec5" %>
...
<myCustom:MossMenu ID="CurrentNav" runat="server" datasourceID="SiteMapDS"
orientation="Vertical" UseCompactMenus="true" StaticDisplayLevels="6"
MaximumDynamicDisplayLevels="0" StaticSubMenuIndent="5" ItemWrap="false"
AccessKey="3" CssClass="leftNav"
SkipLinkText="<%$Resources:cms,masterpages_skiplinktext%>">
<LevelMenuItemStyles>
<asp:MenuItemStyle CssClass="Nav" />
<asp:MenuItemStyle CssClass="SecNav" />
</LevelMenuItemStyles>
<StaticHoverStyle CssClass="leftNavHover"/>
<StaticSelectedStyle CssClass="leftNavSelected"/>
<DynamicMenuStyle CssClass="leftNavFlyOuts" />
<DynamicMenuItemStyle CssClass="leftNavFlyOutsItem"/>
<DynamicHoverStyle CssClass="leftNavFlyOutsHover"/>
</myCustom:MossMenu>
<PublishingNavigation:PortalSiteMapDataSource ID="SiteMapDS" Runat="server"
SiteMapProvider="CurrentNavSiteMapProvider" EnableViewState="true"
StartFromCurrentNode="true" ShowStartingNode="false"/>
...
I followed the excellent step-by-step instructions to create my custom web part in the comments section of the MossMenu webpart at "Wednesday, September 19, 2007 7:20 AM by Roel". In my googling, I also found something to configure a Sharepoint site to display exceptions in the same lovely way that ASP.NET does by making the web.config changes here.
I decided to call my custom behavior a "compact menu" so I created a UseCompactMenus property on the control. If you don't set this attribute in the markup to true, the control will behave identically to an AspMenu control.
My application has the user always starting from the home page at the site map root. I can have the custom control store the initial (complete) site map when the root page is displayed. This is stored in a static string for use in the customizing behavior. If you application doesn't follow this assumption, the control will not work as expected.
On the initial application page, only the direct child pages to the root page are displayed in the menu. Clicking on these menu nodes will open all the child nodes under it but keeps the sibling nodes "closed". If you click on one of the other sibling nodes, it collapses the current node and it opens the newly selected node. That's it, enjoy!!
using System;
using System.Text;
using System.ComponentModel;
using System.Collections.Generic;
using System.Security.Permissions;
using System.Xml;
using System.Xml.Serialization;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Security;
namespace YourCompany.CustomWebParts
{
[AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
[SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
[Designer(typeof(MossMenuDesigner))]
[ToolboxData("<{0}:MossMenu runat=\"server\" />")]
public class MossMenu : System.Web.UI.WebControls.Menu
{
private string idPrefix;
// a url->menuItem dictionary
private Dictionary<string, System.Web.UI.WebControls.MenuItem> menuItemDictionary =
new Dictionary<string, System.Web.UI.WebControls.MenuItem>(StringComparer.OrdinalIgnoreCase);
private bool customSelectionEnabled = true;
private bool selectStaticItemsOnly = true;
private bool performTargetBinding = true;
//** Variables used for compact menu behavior **//
private bool useCompactMenus = false;
private static bool showStartingNode;
private static string originalSiteMap;
/// <summary>
/// Controls whether or not the control performs compacting of the site map to display only ancestor and child nodes of the selected and first level root childern.
/// </summary>
[Category("Behavior")]
public bool UseCompactMenus
{
get
{
return this.useCompactMenus;
}
set
{
this.useCompactMenus = value;
}
}
/// <summary>
/// Controls whether or not the control performs custom selection/highlighting.
/// </summary>
[Category("Behavior")]
public bool CustomSelectionEnabled
{
get
{
return this.customSelectionEnabled;
}
set
{
this.customSelectionEnabled = value;
}
}
/// <summary>
/// Controls whether only static items may be selected or if
/// dynamic (fly-out) items may be selected too.
/// </summary>
[Category("Behavior")]
public bool SelectStaticItemsOnly
{
get
{
return this.selectStaticItemsOnly;
}
set
{
this.selectStaticItemsOnly = value;
}
}
/// <summary>
/// Controls whether or not to bind the Target property of any menu
/// items to the Target property in the SiteMapNode's Attributes
/// collection.
/// </summary>
[Category("Behavior")]
public bool PerformTargetBinding
{
get
{
return this.performTargetBinding;
}
set
{
this.performTargetBinding = value;
}
}
/// <summary>
/// Gets the ClientID of this control.
/// </summary>
public override string ClientID
{
[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
get
{
if (this.idPrefix == null)
{
this.idPrefix = SPUtility.GetNewIdPrefix(this.Context);
}
return SPUtility.GetShortId(this.idPrefix, this);
}
}
[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
protected override void OnMenuItemDataBound(MenuEventArgs e)
{
base.OnMenuItemDataBound(e);
if (this.customSelectionEnabled)
{
// store in the url->item dictionary
this.menuItemDictionary[e.Item.NavigateUrl] = e.Item;
}
if (this.performTargetBinding)
{
// try to bind to the Target property if the data item is a SiteMapNode
SiteMapNode smn = e.Item.DataItem as SiteMapNode;
if (smn != null)
{
string target = smn["Target"];
if (!string.IsNullOrEmpty(target))
{
e.Item.Target = target;
}
}
}
}
/// <id guid="08e034e7-5872-4a31-a771-84cac1dcd53d" />
/// <owner alias="MarkWal">
/// </owner>
[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
protected override void OnPreRender(System.EventArgs e)
{
SiteMapDataSource dataSource = this.GetDataSource() as SiteMapDataSource;
SiteMapProvider provider = (dataSource != null) ? dataSource.Provider : null;
if (useCompactMenus && dataSource != null && provider != null)
{
showStartingNode = dataSource.ShowStartingNode;
SiteMapNodeCollection rootChildNodes = provider.RootNode.ChildNodes;
if (provider.CurrentNode.Equals(provider.RootNode))
{
//** Store original site map for future use in compacting menus **//
if (originalSiteMap == null)
{
//Store original SiteMapXML for future adjustments:
XmlDocument newSiteMapDoc = new XmlDocument();
newSiteMapDoc.LoadXml("<?xml version='1.0' ?>"
+ "<siteMapNode title='" + provider.RootNode.Title
+ "' url='" + provider.RootNode.Url
+ "' />");
foreach (SiteMapNode node in rootChildNodes)
{
XmlNode newNode = GetXmlSiteMapNode(newSiteMapDoc.DocumentElement, node);
newSiteMapDoc.DocumentElement.AppendChild(newNode);
//Create XML for all the child nodes for selected menu item:
NavigateSiteMap(newNode, node);
}
originalSiteMap = newSiteMapDoc.OuterXml;
}
//This is set to only display the child nodes of the root node on first view:
this.StaticDisplayLevels = 1;
}
else
{
//
//Adjust site map for this page
//
XmlDocument newSiteMapDoc = InitializeNewSiteMapXml(provider, rootChildNodes);
//Clear the current default site map:
this.DataSourceID = null;
//Create the new site map data source
XmlDataSource newSiteMap = new XmlDataSource();
newSiteMap.ID = "XmlDataSource1";
newSiteMap.EnableCaching = false; //Required to prevent redisplay of the previous menu
//Add bindings for dynamic site map:
MenuItemBindingCollection bindings = this.DataBindings;
bindings.Clear();
MenuItemBinding binding = new MenuItemBinding();
binding.DataMember = "siteMapNode";
binding.TextField = "title";
binding.Text = "title";
binding.NavigateUrlField = "url";
binding.NavigateUrl = "url";
binding.ValueField = "url";
binding.Value = "url";
bindings.Add(binding);
//Bind menu to new site map:
this.DataSource = newSiteMap;
//Assign the newly created dynamic site map:
((XmlDataSource)this.DataSource).Data = newSiteMapDoc.OuterXml;
/** this expression removes the root if initialized: **/
if (!showStartingNode)
((XmlDataSource)this.DataSource).XPath = "/siteMapNode/siteMapNode";
/** Re-initialize menu data source with new site map: **/
this.DataBind();
/** Find depth of current node: **/
int depth = 0;
SiteMapNode currNode = provider.CurrentNode;
do
{
depth++;
currNode = currNode.ParentNode;
}
while (currNode != null);
//Set the StaticDisplayLevels to match the current depth:
if (depth >= this.StaticDisplayLevels)
this.StaticDisplayLevels = depth;
}
}
base.OnPreRender(e);
// output some script to override the default menu flyout behaviour; this helps to avoid
// intermittent "Operation Aborted" errors
Page.ClientScript.RegisterStartupScript(
typeof(MossMenu),
"overrideMenu_HoverStatic",
"if (typeof(overrideMenu_HoverStatic) == 'function' && typeof(Menu_HoverStatic) == 'function')\n" +
"{\n" +
"_spBodyOnLoadFunctionNames.push('enableFlyoutsAfterDelay');\n" +
"Menu_HoverStatic = overrideMenu_HoverStatic;\n" +
"}\n",
true);
// output some script to avoid a known issue with SSL Termination and the ASP.NET
// Menu implementation. http://support.microsoft.com/?id=910444
Page.ClientScript.RegisterStartupScript(
typeof(MossMenu),
"MenuHttpsWorkaround_" + this.ClientID,
this.ClientID + "_Data.iframeUrl='/_layouts/images/blank.gif';",
true);
// adjust the fly-out indicator arrow direction for locale if not already set
if (this.Orientation == System.Web.UI.WebControls.Orientation.Vertical &&
((string.IsNullOrEmpty(this.StaticPopOutImageUrl) && this.StaticEnableDefaultPopOutImage) ||
(string.IsNullOrEmpty(this.DynamicPopOutImageUrl) && this.DynamicEnableDefaultPopOutImage)))
{
SPWeb currentWeb = SPContext.Current.Web;
if (currentWeb != null)
{
uint localeId = currentWeb.Language;
bool isBidiWeb = SPUtility.IsRightToLeft(currentWeb, currentWeb.Language);
string arrowUrl = "/_layouts/images/" + (isBidiWeb ? "largearrowleft.gif" : "largearrowright.gif");
if (string.IsNullOrEmpty(this.StaticPopOutImageUrl) && this.StaticEnableDefaultPopOutImage)
{
this.StaticPopOutImageUrl = arrowUrl;
}
if (string.IsNullOrEmpty(this.DynamicPopOutImageUrl) && this.DynamicEnableDefaultPopOutImage)
{
this.DynamicPopOutImageUrl = arrowUrl;
}
}
}
if (provider == null)
{
// if we're not attached to a SiteMapDataSource we'll just leave everything alone
return;
}
else if (this.customSelectionEnabled)
{
MenuItem selectedMenuItem = this.SelectedItem;
SiteMapNode currentNode = provider.CurrentNode;
// if no menu item is presently selected, we need to work our way up from the current
// node until we can find a node in the menu item dictionary
while (selectedMenuItem == null && currentNode != null)
{
this.menuItemDictionary.TryGetValue(currentNode.Url, out selectedMenuItem);
currentNode = currentNode.ParentNode;
}
if (this.selectStaticItemsOnly)
{
// only static items may be selected, keep moving up until we find an item
// that falls within the static range
while (selectedMenuItem != null && selectedMenuItem.Depth >= this.StaticDisplayLevels)
{
selectedMenuItem = selectedMenuItem.Parent;
}
// if we found an item to select, go ahead and select (highlight) it
if (selectedMenuItem != null && selectedMenuItem.Selectable)
{
selectedMenuItem.Selected = true;
}
}
}
}
private XmlDocument InitializeNewSiteMapXml(SiteMapProvider provider, SiteMapNodeCollection rootChildNodes)
{
/** Find the level 1 ancestor node of the current node: **/
SiteMapNode levelOneAncestorOfSelectedNode = null;
SiteMapNode currNode = provider.CurrentNode;
do
{
levelOneAncestorOfSelectedNode = (currNode.ParentNode == null ? levelOneAncestorOfSelectedNode : currNode);
currNode = currNode.ParentNode;
}
while (currNode != null);
/** Initialize base SiteMapXML **/
XmlDocument newSiteMapDoc = new XmlDocument();
newSiteMapDoc.LoadXml(originalSiteMap);
/** Prune out the childern nodes that shouldn't display: **/
currNode = provider.CurrentNode;
do
{
if (currNode.ParentNode != null)
{
SiteMapNodeCollection currNodeSiblings = currNode.ParentNode.ChildNodes;
foreach (SiteMapNode siblingNode in currNodeSiblings)
{
if (siblingNode.HasChildNodes)
{
if (provider.CurrentNode.Equals(siblingNode))
{
//Remove all the childerns child nodes from display:
SiteMapNodeCollection currNodesChildren = siblingNode.ChildNodes;
foreach (SiteMapNode childNode in currNodesChildren)
{
XmlNode currentXmNode = GetCurrentXmlNode(newSiteMapDoc, childNode);
DeleteChildNodes(currentXmNode);
}
}
else if (!provider.CurrentNode.IsDescendantOf(siblingNode)
&& !levelOneAncestorOfSelectedNode.Equals(siblingNode))
{
XmlNode currentXmNode = GetCurrentXmlNode(newSiteMapDoc, siblingNode);
DeleteChildNodes(currentXmNode);
}
}
}
}
currNode = currNode.ParentNode;
}
while (currNode != null);
return newSiteMapDoc;
}
private XmlNode GetCurrentXmlNode(XmlDocument newSiteMapDoc, SiteMapNode node)
{
//Find this node in the original site map:
XmlNode currentXmNode = newSiteMapDoc.DocumentElement.SelectSingleNode(
"//siteMapNode[#url='"
+ node.Url
+ "']");
return currentXmNode;
}
private void DeleteChildNodes(XmlNode currentXmNode)
{
if (currentXmNode != null && currentXmNode.HasChildNodes)
{
//Remove child nodes:
XmlNodeList xmlNodes = currentXmNode.ChildNodes;
int lastNodeIndex = xmlNodes.Count - 1;
for (int i = lastNodeIndex; i >= 0; i--)
{
currentXmNode.RemoveChild(xmlNodes[i]);
}
}
}
private XmlNode GetXmlSiteMapNode(XmlNode currentDocumentNode, SiteMapNode currentNode)
{
XmlElement newNode = currentDocumentNode.OwnerDocument.CreateElement("siteMapNode");
XmlAttribute newAttr = currentDocumentNode.OwnerDocument.CreateAttribute("title");
newAttr.InnerText = currentNode.Title;
newNode.Attributes.Append(newAttr);
newAttr = currentDocumentNode.OwnerDocument.CreateAttribute("url");
newAttr.InnerText = currentNode.Url;
newNode.Attributes.Append(newAttr);
return newNode;
}
private void NavigateSiteMap(XmlNode currentDocumentNode, SiteMapNode currentNode)
{
foreach (SiteMapNode node in currentNode.ChildNodes)
{
//Add this node to structure:
XmlNode newNode = GetXmlSiteMapNode(currentDocumentNode, node);
currentDocumentNode.AppendChild(newNode);
if (node.HasChildNodes)
{
//Make a recursive call to add any child nodes:
NavigateSiteMap(newNode, node);
}
}
}
}
[PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2117:AptcaTypesShouldOnlyExtendAptcaBaseTypes")]
public sealed class MossMenuDesigner : MenuDesigner
{
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
protected override void DataBind(BaseDataBoundControl dataBoundControl)
{
try
{
dataBoundControl.DataBind();
}
catch
{
base.DataBind(dataBoundControl);
}
}
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public override string GetDesignTimeHtml()
{
System.Web.UI.WebControls.Menu menu = (System.Web.UI.WebControls.Menu)ViewControl;
int oldDisplayLevels = menu.MaximumDynamicDisplayLevels;
string designTimeHtml = string.Empty;
try
{
menu.MaximumDynamicDisplayLevels = 0;
// ASP.NET MenuDesigner has some dynamic/static item trick in design time
// to show dynamic item in design time. We only want to show preview without
// dynamic menu items.
designTimeHtml = base.GetDesignTimeHtml();
}
catch (Exception e)
{
designTimeHtml = GetErrorDesignTimeHtml(e);
}
finally
{
menu.MaximumDynamicDisplayLevels = oldDisplayLevels;
}
return designTimeHtml;
}
}
}
I personally don't like the html that the default menu provides (table based layout).
Fortunately the SharePoint team has released the code for that control.
What we have done is to include that code in a project and have overridden the render method to do whatever we want. This give you the flexibility to define the exact relationship between parents that needs to be display as well as setting the styles on any divs you create.
On the down side you are now coding, not configuring and a change needs to be made to the master page you are using to use the control.
Worth it in my opinion. This is now a standard change we make for any site.
The approach we used to accomplish the affect you are looking for was to use the CSS Friendly Control Adapters. The adapters change the HTML that is rendered without changing the controls you used on your pages. You may need to tweak the menu adapter a little bit in order to get the layout you want. It only took a few lines of code for us. Once you get that working, you can use CSS to obtain the behavior you describe.

Resources