Accessing the object/row being edited in Dynamic Data - asp.net

I'm modifying the "Edit.aspx" default page template used by ASP.NET Dynamic Data and adding some additional controls. I know that I can find the type of object being edited by looking at DetailsDataSource.GetTable().EntityType, but how can I see the actual object itself? Also, can I change the properties of the object and tell the data context to submit those changes?

Maybe you have found a solution already, however I'd like to share my expresience on this.
It turned out to be a great pita, but I've managed to obtain the editing row. I had to extract the DetailsDataSource WhereParameters and then create a query in runtime.
The code below works for tables with a single primary key. If you have compound keys, I guess, it will require modifications:
Parameter param = null;
foreach(object item in (DetailsDataSource.WhereParameters[0] as DynamicQueryStringParameter).GetWhereParameters(DetailsDataSource)) {
param = (Parameter)item;
break;
}
IQueryable query = DetailsDataSource.GetTable().GetQuery();
ParameterExpression lambdaArgument = Expression.Parameter(query.ElementType, "");
object paramValue = Convert.ChangeType(param.DefaultValue, param.Type);
Expression compareExpr = Expression.Equal(
Expression.Property(lambdaArgument, param.Name),
Expression.Constant(paramValue)
);
Expression lambda = Expression.Lambda(compareExpr, lambdaArgument);
Expression filteredQuery = Expression.Call(typeof(Queryable), "Where", new Type[] { query.ElementType }, query.Expression, lambda);
var WANTED = query.Provider.CreateQuery(filteredQuery).Cast<object>().FirstOrDefault<object>();

If it's a DD object you may be able to use FieldTemplateUserControl.FindFieldTemplate(controlId). Then if you need to you can cast it as an ITextControl to manipulate data.
Otherwise, try using this extension method to find the child control:
public static T FindControl<T>(this Control startingControl, string id) where T : Control
{
T found = startingControl.FindControl(id) as T;
if (found == null)
{
found = FindChildControl<T>(startingControl, id);
}
return found;
}

I found another solution, the other ones did not work.
In my case, I've copied Edit.aspx in /CustomPages/Devices/
Where Devices is the name of the table for which I want this custom behaviour.
Add this in Edit.aspx -> Page_Init()
DetailsDataSource.Selected += entityDataSource_Selected;
Add this in Edit.aspx :
protected void entityDataSource_Selected(object sender, EntityDataSourceSelectedEventArgs e)
{
Device device = e.Results.Cast<Device>().First();
// you have the object/row being edited !
}
Just change Device to your own table name.

Related

Using InMemory table as a form datasource in Dynamics 365 f&o

I'm obtaining data from an external service and inserting it into an inMemory table (Table_movieTemp), which I use as a datasource on a form (Form_MovieSearch_ds):
[FormControlEventHandler(formControlStr(Form_MovieSearch, FormCommandButtonControl1), FormControlEventType::Clicked)]
public static void FormCommandButtonControl1_OnClicked(FormControl sender, FormControlEventArgs e)
{
FormDataSource Form_MovieSearch_ds = formRun.dataSource();
System.Collections.IEnumerable data = ClassLibrary1.Program::CallRestService();
var enumerator = data.getEnumerator();
while(enumerator.moveNext())
{
MovieRentalService.TmdbMovie item = enumerator.get_current();
Table_movieTemp.Description = item.Description;
Table_movieTemp.ReleaseDate = today();
Table_movieTemp.Title = item.Title;
Table_movieTemp.Rating = item.Rating;
Table_movieTemp.existsAlready = Table_Movie::exist(item.Title);
insertList.add(movieTemp);
}
ttsbegin;
insertList.insertDatabase();
ttscommit;
while select Table_movieTemp
{
info(strFmt("Name: %1,", Table_movieTemp.Title));
}
The while loop I used purely to prove the inserts were succesful.
Afterwards I figure I can call the executeQuery on the form which has my temptable as datasource:
FM_MovieSearch_ds.executeQuery();
This did not work and when I searched google I found a solution where I have to pass the TempTable buffer so that I can link it using 'setTmpTable'.
So I added the following call before calling executeQuery():
formRun.BindTable(movieTemp);
Function on my form:
public void BindTable(FM_MovieTemp _movieTempBuffer)
{
_movieTempBuffer.setTmpData(_movieTempBuffer);
}
Now my code compiles and does not generate runtime errors either, but I still don't see any data. Could someone advice what I miss or do wrong?
The use of in-memory tables in forms has been around for 25 years, and you will find several uses in the standard application.
From the CustVendAgingStatistics form:
void calcAgingStatistics(boolean _research)
{
CustVendAgingStatistics custVendAgingStatistics = CustVendAgingStatistics::construct(linkedCustVendTable, graphData.loadDefName(), graphData.perInvoiceDate());
custVendAgingStatistics.calcStatistic();
tmpAccountSum.setTmpData(custVendAgingStatistics.tmpAccountsum());
if (_research)
{
tmpAccountSum_ds.research();
}
}
Another nice example is found here.
The method:
Insert the records in a separate method, return the local buffer.
In the calling method call setTmpData with the return value.
Research the datasource
In your code I see the use of InsertRecordList, do not use that on in-memory temporary tables, it makes no sense.
Also _movieTempBuffer.setTmpData(_movieTempBuffer) does not do anyting useful as it operates on itself.
Also good style is not do a lot in onClicked methods and other event methods, call proper methods to do the hard work instead.

SDL Tridion 2011: Dynamically fill or add a metadata field using a C# TBB

Is it possible to set the value of a metadata field dynamically from a TBB? Or is it possible to dynamically add a metadata field that does not necessarily exist on a schema from a TBB?
The reason I want to do this is that I am using DD4T and I want to have the breadcrumb automatically added into the DD4T xml.
I have tried the following:
public override void Transform(Engine engine, Package package)
{
Initialize(engine,package);
var page = GetPage();
string output = page.OrganizationalItem.Title;
var parent = page.OrganizationalItem as StructureGroup;
while (parent != null)
{
output = GetLinkToStructureGroupIndexPage(parent) + Separator + output;
parent = parent.OrganizationalItem as StructureGroup;
}
// I tried this to dynamically add the field
//var metadata = page.Metadata.OwnerDocument.CreateElement("breadcrumb");
//metadata.InnerText = output;
//page.Metadata.AppendChild(metadata);
//I tried this to dynamically set an existing field on the schema
foreach (XmlNode xml in page.Metadata)
{
Log.Debug("Metadata field:" +xml.Name);
if(xml.Name == "breadcrumb")
{
xml.InnerText = output;
}
}
package.PushItem(Package.PageName, package.CreateTridionItem(ContentType.Page, page));
}
However, neither of these methods seem to work. Is this impossible?
DD4T has utility class FieldsBuilder with AddFields method where you can inject additional metadata. DD4T has a TBB which does update component metadata from Folder Metadata and it is called InheritMetadataComponent.
You could take a look at this here and you could implement the same:
http://code.google.com/p/dynamic-delivery-4-tridion/source/browse/trunk/dotnet/DD4T.Templates/InheritMetadataComponent.cs
FieldsBuilder.AddFields(component.MetadataFields, tcmFields, 1, false, mergeAction, Manager);
The easiest approach is to create a template class which implements DD4T.Templates.Base.BasePageTemplate. In that class, you implement the method TransformPage, which takes a DD4T page as its argument. You can access the 'TCM page' using the method GetTcmPage().
Example:
using TCM = Tridion.ContentManager.CommunicationManagement;
using Dynamic = DD4T.ContentModel;
public class MyTemplate : BasePageTemplate
{
protected override void TransformPage(Dynamic.Page page)
{
TCM.Page tcmPage = GetTcmPage();
string breadCrumbs = GetBreadCrumbs (tcmPage); // TODO: implement GetBreadCrumbs
Field f = new Field();
f.Name = "breadCrumbs";
f.FieldType = FieldType.Text;
f.Values.Add(breadCrumbs);
page.MetadataFields.Add("breadCrumbs", f);
}
}
page.MetadataFields.Add(name, field); should work if your template extends the DD4T.Templates.Base.BasePageTemplate
You can also take a look at the source of the Add inherited metadata to page TBB in DD4T, that also shows a way of adding Metadata which gets published to the broker.

ILGenerator. Whats wrong with this Code

I am trying to build a dynamic Property Accessor. Want something which is like really fast as close to calling the actually Property. Dont want to go the Reflection route as its very slow. So i opted to using DynamicAssembly and inject IL using ILGenerator. Below is the ILGenerator related code which seems to work
Label nulllabel = getIL.DefineLabel();
Label returnlabel = getIL.DefineLabel();
//_type = targetGetMethod.ReturnType;
if (methods.Count > 0)
{
getIL.DeclareLocal(typeof(object));
getIL.DeclareLocal(typeof(bool));
getIL.Emit(OpCodes.Ldarg_1); //Load the first argument
//(target object)
//Cast to the source type
getIL.Emit(OpCodes.Castclass, this.mTargetType);
//Get the property value
foreach (var methodInfo in methods)
{
getIL.EmitCall(OpCodes.Call, methodInfo, null);
if (methodInfo.ReturnType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
}
getIL.Emit(OpCodes.Stloc_0); //Store it
getIL.Emit(OpCodes.Br_S,returnlabel);
getIL.MarkLabel(nulllabel);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Stloc_0);
getIL.MarkLabel(returnlabel);
getIL.Emit(OpCodes.Ldloc_0);
}
else
{
getIL.ThrowException(typeof(MissingMethodException));
}
getIL.Emit(OpCodes.Ret);
So above get the first argument which is the object that contains the property. the methods collection contains the nested property if any. for each property i use EmitCall which puts the the value on the stack and then i try to box it. This works like a charm.
The only issue is if you have a property like Order.Instrument.Symbol.Name and assume that Instrument object is null. Then the code will throw an null object exception.
So this what i did, i introduced a null check
foreach (var methodInfo in methods)
{
getIL.EmitCall(OpCodes.Call, methodInfo, null);
getIL.Emit(OpCodes.Stloc_0);
getIL.Emit(OpCodes.Ldloc_0);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Ceq);
getIL.Emit(OpCodes.Stloc_1);
getIL.Emit(OpCodes.Ldloc_1);
getIL.Emit(OpCodes.Brtrue_S, nulllabel);
getIL.Emit(OpCodes.Ldloc_0);
if (methodInfo.ReturnType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
}
Now this code breaks saying That the object/memory is corrupted etc. So what exactly is wrong with this code. Am i missing something here.
Thanks in Advance.
Previously, if you had consecutive properties P returning string and then Q returning int, you would get something like this:
...
call P // returns string
call Q // requires a string on the stack, returns an int
box
...
Now you have something like this:
...
call P // returns string
store // stores to object
... // load, compare to null, etc.
load // loads an *object*
call Q // requires a *string* on the stack
store // stores to object *without boxing*
...
So I see two clear problems:
You are calling methods in such a way that the target is only known to be an object, not a specific type which has that method.
You are not boxing value types before storing them to a local of type object.
These can be solved by reworking your logic slightly. There are also a few other minor details you could clean up:
Rather than ceq followed by brtrue, just use beq.
There's no point in doing Stloc_1 followed by Ldloc_1 rather than just using the value on the stack since that local isn't used anywhere else.
Incorporating these changes, here's what I'd do:
Type finalType = null;
foreach (var methodInfo in methods)
{
finalType = methodInfo.ReturnType;
getIL.EmitCall(OpCodes.Call, methodInfo, null);
if (!finalType.IsValueType)
{
getIL.Emit(OpCodes.Dup);
getIL.Emit(OpCodes.Ldnull);
getIL.Emit(OpCodes.Beq_S, nulllabel);
}
}
if (finalType.IsValueType)
{
getIL.Emit(OpCodes.Box, methodInfo.ReturnType);
//Box if necessary
}
getIL.Emit(OpCodes.Br_S, returnLabel);
getIL.MarkLabel(nulllabel);
getIL.Emit(OpCodes.Pop);
getIL.Emit(OpCodes.Ldnull);
getIL.MarkLabel(returnlabel);
Note that we can get rid of both locals since we now just duplicate the top value on the stack before comparing against null.

Change Single URL query string value

I have an ASP.NET page which takes a number of parameters in the query string:
search.aspx?q=123&source=WebSearch
This would display the first page of search results. Now within the rendering of that page, I want to display a set of links that allow the user to jump to different pages within the search results. I can do this simply by append &page=1 or &page=2 etc.
Where it gets complicated is that I want to preserve the input query string from the original page for every parameter except the one that I'm trying to change. There may be other parameters in the url used by other components and the value I'm trying to replace may or may not already be defined:
search.aspx?q=123&source=WebSearch&page=1&Theme=Blue
In this case to generate a link to the next page of results, I want to change page=1 to page=2 while leaving the rest of the query string unchanged.
Is there a builtin way to do this, or do I need to do all of the string parsing/recombining manually?
You can't modify the QueryString directly as it is readonly. You will need to get the values, modify them, then put them back together. Try this:
var nameValues = HttpUtility.ParseQueryString(Request.QueryString.ToString());
nameValues.Set("page", "2");
string url = Request.Url.AbsolutePath;
string updatedQueryString = "?" + nameValues.ToString();
Response.Redirect(url + updatedQueryString);
The ParseQueryString method returns a NameValueCollection (actually it really returns a HttpValueCollection which encodes the results, as I mention in an answer to another question). You can then use the Set method to update a value. You can also use the Add method to add a new one, or Remove to remove a value. Finally, calling ToString() on the name NameValueCollection returns the name value pairs in a name1=value1&name2=value2 querystring ready format. Once you have that append it to the URL and redirect.
Alternately, you can add a new key, or modify an existing one, using the indexer:
nameValues["temp"] = "hello!"; // add "temp" if it didn't exist
nameValues["temp"] = "hello, world!"; // overwrite "temp"
nameValues.Remove("temp"); // can't remove via indexer
You may need to add a using System.Collections.Specialized; to make use of the NameValueCollection class.
You can do this without all the overhead of redirection (which is not inconsiderable). My personal preference is to work with a NameValueCollection which a querystring really is, but using reflection:
// reflect to readonly property
PropertyInfo isReadOnly = typeof(System.Collections.Specialized.NameValueCollection).GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
// make collection editable
isReadOnly.SetValue(this.Request.QueryString, false, null);
// remove
this.Request.QueryString.Remove("foo");
// modify
this.Request.QueryString.Set("bar", "123");
// make collection readonly again
isReadOnly.SetValue(this.Request.QueryString, true, null);
Using this QueryStringBuilder helper class, you can grab the current QueryString and call the Add method to change an existing key/value pair...
//before: "?id=123&page=1&sessionId=ABC"
string newQueryString = QueryString.Current.Add("page", "2");
//after: "?id=123&page=2&sessionId=ABC"
Use the URIBuilder Specifically the link textQuery property
I believe that does what you need.
This is pretty arbitrary, in .NET Core at least. And it all boils down to asp-all-route-data
Consider the following trivial example (taken from the "paginator" view model I use in virtually every project):
public class SomeViewModel
{
public Dictionary<string, string> NextPageLink(IQueryCollection query)
{
/*
* NOTE: how you derive the "2" is fully up to you
*/
return ParseQueryCollection(query, "page", "2");
}
Dictionary<string, string> ParseQueryCollection(IQueryCollection query, string replacementKey, string replacementValue)
{
var dict = new Dictionary<string, string>()
{
{ replacementKey, replacementValue }
};
foreach (var q in query)
{
if (!string.Equals(q.Key, replacementKey, StringComparison.OrdinalIgnoreCase))
{
dict.Add(q.Key, q.Value);
}
}
return dict;
}
}
Then to use in your view, simply pass the method the current request query collection from Context.Request:
<a asp-all-route-data="#Model.NextPageLink(Context.Request.Query)">Next</a>

Use DisplayNameAttribute in ASP.NET

I want to bind a List to a GridView on a web page, but override the way the property names display via annotation. I thought System.ComponentModel would work, but this doesn't seem to work. Is this only meant for Windows Forms?:
using System.ComponentModel;
namespace MyWebApp
{
public class MyCustomClass
{
[DisplayName("My Column")]
public string MyFirstProperty
{
get { return "value"; }
}
public MyCustomClass() {}
}
Then on the page:
protected void Page_Load(object sender, EventArgs e)
{
IList<MyCustomClass> myCustomClasses = new List<MyCustomClass>
{
new MyCustomClass(),
new MyCustomClass()
};
TestGrid.DataSource = myCustomClasses;
TestGrid.DataBind();
}
This renders with "MyFirstProperty" as the column header rather than "My Column." Isn't this supposed to work?
When using .net 4 or later you can use gridview1.enabledynamicdata(typeof(mytype)). I haven't looked at all the types you can use there but I know the [displayname("somename")] works well but the [browsable(false)] doesn't omit the column from the grid. It looks like a knit one slip one from MS. at least you can easily rename column names and to omit a column I just declare a variable instead of using a property. It has the same effect...
Just by the way, using the designer to create columns is the easy way out but to just show a different column name takes way to much time especially with classes with many fields.
What SirDemon said...
The answer appears to be no, you can't. At least not out of the box.
The System.Web.UI.WebControls.GridView uses reflected property's name:
protected virtual AutoGeneratedField CreateAutoGeneratedColumn(AutoGeneratedFieldProperties fieldProperties)
{
AutoGeneratedField field = new AutoGeneratedField(fieldProperties.DataField);
string name = fieldProperties.Name; //the name comes from a PropertyDescriptor
((IStateManager) field).TrackViewState();
field.HeaderText = name; //<- here's reflected property name
field.SortExpression = name;
field.ReadOnly = fieldProperties.IsReadOnly;
field.DataType = fieldProperties.Type;
return field;
}
While System.Windows.Forms.DataGridView uses DisplayName if available:
public DataGridViewColumn[] GetCollectionOfBoundDataGridViewColumns()
{
...
ArrayList list = new ArrayList();
//props is a collection of PropertyDescriptors
for (int i = 0; i < this.props.Count; i++)
{
if (...)
{
DataGridViewColumn dataGridViewColumnFromType = GetDataGridViewColumnFromType(this.props[i].PropertyType);
...
dataGridViewColumnFromType.Name = this.props[i].Name;
dataGridViewColumnFromType.HeaderText = !string.IsNullOrEmpty(this.props[i].DisplayName) ? this.props[i].DisplayName : this.props[i].Name;
}
}
DataGridViewColumn[] array = new DataGridViewColumn[list.Count];
list.CopyTo(array);
return array;
}
Unfortunately, while you can override the CreateAutoGeneratedColumn, neither the missing DisplayName nor underlying property descriptor gets passed, and you can't override CreateAutoGeneratedColumns (although you could CreateColumns).
This means you'd have to iterate over reflected properties yourself and in some other place.
If all you care about is the header text in GridView, just use the HeaderText property of each field you bind. If you're autogenerating the columns, you just set the HeaderText after you've bound the GridView.
If you want a GridView that takes into account some attribute you placed on the properties of your bound class, I believe you'll need to create your own GridView.
I may be wrong, but I've not seen any ASP.NET Grid from control vendors (at least Telerik , Janus Systems and Infragistics) do that. If you do it, maybe sell the idea to them.
Are you using .net4, what you need to do is to set enabledynamicdata on the grid view to true.
You can do it now on asp.net mvc2. It works just like that

Resources