Megosztás a következőn keresztül:


Testreszabott rendezési felhasználói felület létrehozása (C#)

által Scott Mitchell

PDF letöltése

A rendezett adatok hosszú listájának megjelenítésekor nagyon hasznos lehet a kapcsolódó adatok csoportosítása elválasztó sorok bevezetésével. Ebben az oktatóanyagban meglátjuk, hogyan hozhatunk létre ilyen rendezési felhasználói felületet.

Bevezetés

Ha rendezett adatok hosszú listáját jeleníti meg, ahol csak néhány különböző érték van a rendezett oszlopban, a végfelhasználó nehezen tudja megállapítani, hogy pontosan hol fordulnak elő a különbséghatárok. Például 81 termék van az adatbázisban, de csak kilenc különböző kategória (nyolc egyedi kategória plusz az NULL opció). Vegyük egy olyan felhasználó esetét, aki érdeklődik a tenger gyümölcsei kategóriába tartozó termékek vizsgálata iránt. Egy olyan oldalon, amely az összes terméket egyetlen GridView-ban sorolja fel, a felhasználó úgy dönthet, hogy a legjobb megoldás az, ha kategóriák szerint rendezi az eredményeket, amelyek az összes tenger gyümölcseit csoportosítják. A kategória szerinti rendezés után a felhasználónak át kell kutatnia a listát, és meg kell keresnie, hogy a tenger gyümölcsei által csoportosított termékek hol kezdődnek és végződnek. Mivel az eredmények a kategória neve szerint ábécé sorrendben vannak rendezve, a tenger gyümölcsei termékek megtalálása nem nehéz, de még mindig alaposan át kell vizsgálni a rácsban lévő elemek listáját.

A rendezett csoportok közötti határok kiemelése érdekében sok webhely olyan felhasználói felületet használ, amely elválasztót ad az ilyen csoportok között. Az 1. ábrán láthatókhoz hasonló elválasztók lehetővé teszik a felhasználó számára, hogy gyorsabban megtaláljon egy adott csoportot és azonosítsa annak határait, valamint megbizonyosodjon arról, hogy milyen különálló csoportok léteznek az adatokban.

Minden kategóriacsoport egyértelműen azonosítva van

1. ábra: Minden kategóriacsoport egyértelműen azonosítva van (kattintson a teljes méretű kép megtekintéséhez)

Ebben az oktatóanyagban meglátjuk, hogyan hozhatunk létre ilyen rendezési felhasználói felületet.

1. lépés: Szabványos, rendezhető rácsnézet létrehozása

Mielőtt megvizsgálnánk, hogyan bővíthetjük a GridView-t a továbbfejlesztett rendezési felület biztosításához, először hozzon létre egy szabványos, rendezhető GridView-t, amely felsorolja a termékeket. Először nyissa meg a CustomSortingUI.aspx lapot a PagingAndSorting mappában. Adjon hozzá egy GridView-t az oldalhoz, állítsa a ID tulajdonságát értékre ProductList, és kösse egy új ObjectDataSource-hoz. Konfigurálja az ObjectDataSource-t úgy, hogy az ProductsBLL s osztály GetProducts() metódusát használja a rekordok kiválasztásához.

Ezután konfigurálja a GridView-t úgy, hogy csak a ProductName, CategoryName, SupplierNameés a UnitPrice BoundFields, valamint a megszűnt CheckBoxField értéket tartalmazza. Végül állítsa be a GridView-t a rendezés támogatására a GridView intelligens címkéjében található Rendezés engedélyezése jelölőnégyzet bejelölésével (vagy a AllowSorting tulajdonságának beállításával true). Miután elvégezte ezeket a kiegészítéseket az CustomSortingUI.aspx oldalon, a deklaratív jelölésnek az alábbihoz hasonlóan kell kinéznie:

<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ObjectDataSource1" EnableViewState="False">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
    TypeName="ProductsBLL"></asp:ObjectDataSource>

Szánjon egy percet arra, hogy megtekintse eddigi előrehaladásunkat egy böngészőben. A 2. ábra a rendezhető GridView-t mutatja, ha adatai kategóriák szerint vannak ábécé sorrendben rendezve.

A rendezhető GridView adatai kategória szerint vannak rendezve

2. ábra: A rendezhető GridView adatai kategória szerint vannak rendezve (kattintson a teljes méretű kép megtekintéséhez)

2. lépés: Az elválasztó sorok hozzáadásának technikáinak feltárása

Az általános, rendezhető GridView elkészültével már csak az marad, hogy minden egyedi rendezett csoport elé hozzá tudjuk adni az elválasztó sorokat a GridView-hoz. De hogyan lehet ilyen sorokat beszúrni a GridView-ba? Lényegében végig kell iterálnunk a GridView sorain, meg kell határoznunk, hogy hol fordulnak elő különbségek a rendezett oszlop értékei között, majd hozzá kell adnunk a megfelelő elválasztó sort. Ha erre a problémára gondolunk, természetesnek tűnik, hogy a megoldás valahol a GridView eseménykezelőjében RowDataBound rejlik. Amint azt az Adatok alapján egyéni formázás oktatóanyagban tárgyaltuk, ezt az eseménykezelőt gyakran használják a sor adatai alapján sorszintű formázás alkalmazásakor. Az eseménykezelő azonban RowDataBound itt nem a megoldás, mivel ebből az eseménykezelőből nem lehet programozott módon sorokat hozzáadni a GridView-hoz. A GridView gyűjteménye Rows valójában csak olvasható.

További sorok hozzáadásához a GridView-hoz három lehetőség közül választhatunk:

  • Adja hozzá ezeket a metaadat-elválasztó sorokat a GridView-hoz kötött tényleges adatokhoz
  • Miután a GridView az adatokhoz van kötve, adjon hozzá további TableRow példányokat a GridView vezérlőgyűjteményéhez
  • Hozzon létre egy egyéni kiszolgálóvezérlőt, amely kiterjeszti a GridView vezérlőt, és felülbírálja a GridView struktúrájának felépítéséért felelős metódusokat

Egyéni kiszolgálóvezérlő létrehozása lenne a legjobb megoldás, ha erre a funkcióra sok weblapon vagy több webhelyen lenne szükség. Ez azonban elég sok kódot és a GridView belső működésének mélyreható feltárását igényelné. Ezért nem vesszük figyelembe ezt a lehetőséget ebben az oktatóanyagban.

A másik két lehetőség, az elválasztó sorok hozzáadása a GridView-hoz kötött tényleges adatokhoz, és a GridView vezérlőgyűjteményének manipulálása a kötés után - másképp támadja meg a problémát, és megérdemel egy megbeszélést.

Sorok hozzáadása a GridView-hoz kötött adatokhoz

Ha a GridView egy adatforráshoz van kötve, létrehoz egy GridViewRow adatforrás által visszaadott minden rekordhoz. Ezért beszúrhatjuk a szükséges elválasztó sorokat úgy, hogy elválasztó rekordokat adunk hozzá az adatforráshoz, mielőtt a GridView-hoz kötnénk. A 3. ábra szemlélteti ezt a koncepciót.

Az egyik módszer az elválasztó sorok hozzáadása az adatforráshoz

3. ábra: Az egyik technika az elválasztó sorok hozzáadása az adatforráshoz

Az elválasztó rekordok kifejezést idézőjelben használom, mert nincs speciális elválasztó rekord; Ehelyett valahogy meg kell jelölnünk, hogy az adatforrás egy adott rekordja elválasztóként szolgál, nem pedig normál adatsorként. Példáinkban egy ProductsDataTable példányt újra a GridView-hoz kötünk, amely a következőkből ProductRowsáll: . Megjelölhetünk egy rekordot elválasztó sorként, ha a tulajdonságát CategoryID-1 (mivel egy ilyen érték normálisan nem létezhet).

Ennek a technikának a használatához a következő lépéseket kell végrehajtanunk:

  1. A GridView-hoz (egy ProductsDataTable példányhoz) való kötéshez szükséges adatok programozott módon történő lekérése
  2. Az adatok rendezése a GridView s SortExpression és SortDirection tulajdonságok alapján
  3. Iterálja a ProductsRowsProductsDataTable, és keresse meg, hol vannak a különbségek a rendezett oszlopban
  4. Minden csoporthatáron injektáljon be egy elválasztó rekordpéldányt ProductsRow a DataTable-be, amelybe be van CategoryID állítva -1 (vagy bármilyen megjelölést választottak, hogy egy rekordot elválasztó rekordként jelöljenek meg)
  5. Az elválasztó sorok beszúrása után programozott módon kösse az adatokat a GridView-hoz

Ezen az öt lépésen kívül egy eseménykezelőt is meg kell adnunk a GridView s RowDataBound eseményhez. Itt mindegyiket DataRow ellenőrizzük, és megállapítjuk, hogy elválasztó sorról van-e szó, amelynek CategoryID beállítása .-1 Ha igen, akkor valószínűleg módosítani akarjuk a formázását vagy a cellákban megjelenő szöveget.

Ennek a technikának a használata a rendezési csoport határainak beszúrásához a fent ismertetettnél valamivel több munkát igényel, mivel egy eseménykezelőt is meg kell adnia a GridView s Sorting eseményhez, és nyomon kell követnie az SortExpression és értékeket SortDirection .

A GridView s vezérlőgyűjteményének kezelése az adatkötés után

Ahelyett, hogy üzenetet küldenénk az adatoknak, mielőtt a GridView-hoz kötnénk, hozzáadhatjuk az elválasztó sorokat, miután az adatokat a GridView-hoz kötötték. Az adatkötés folyamata felépíti a GridView vezérlési hierarchiáját, amely a valóságban egyszerűen egy Table példány, amely sorok gyűjteményéből áll, amelyek mindegyike cellák gyűjteményéből áll. Pontosabban, a GridView vezérlőgyűjteménye tartalmaz egy Table objektumot a gyökerében, egy GridViewRow (amely az TableRow osztályból származik) a GridView-hoz kötött minden rekordhoz DataSource , és egy objektumot TableCell minden GridViewRow példányban DataSourcea .

Az egyes rendezési csoportok közötti elválasztó sorok hozzáadásához közvetlenül módosíthatjuk ezt a vezérlőhierarchiát, miután létrehoztuk. Biztosak lehetünk abban, hogy a GridView vezérlőhierarchiája utoljára létrejött az oldal megjelenítéséig. Ezért ez a megközelítés felülbírálja az Page s osztály Render metódusát, ekkor a GridView végső vezérlőhierarchiája frissül, hogy tartalmazza a szükséges elválasztó sorokat. A 4. ábra szemlélteti ezt a folyamatot.

Egy alternatív technika a GridView vezérlőhierarchiájának módosítására

4. ábra: Egy alternatív technika manipulálja a GridView vezérlőhierarchiáját (kattintson a teljes méretű kép megtekintéséhez)

Ebben az oktatóanyagban ez utóbbi megközelítést fogjuk használni a rendezési felhasználói élmény testreszabásához.

Megjegyzés:

Az ebben az oktatóanyagban bemutatott kód Teemu Keiski blogbejegyzésében található példán alapul, Playing a Bit with GridView Sort Grouping.

3. lépés: Az elválasztó sorok hozzáadása a GridView vezérlőhierarchiájához

Mivel az elválasztó sorokat csak a vezérlőhierarchia létrehozása és az oldal látogatása során utoljára történő létrehozása után szeretnénk hozzáadni a GridView vezérlőhierarchiájához, ezt a kiegészítést az oldal életciklusának végén, de a tényleges GridView vezérlőhierarchia HTML-be való megjelenítése előtt szeretnénk végrehajtani. A legkésőbbi lehetséges pont, ahol ezt megvalósíthatjuk, az Page s Render osztály eseménye, amelyet a kód mögötti osztályunkban a következő metódusaláírással bírálhatunk felül:

protected override void Render(HtmlTextWriter writer)
{
    // Add code to manipulate the GridView control hierarchy
    base.Render(writer);
}

Az Page osztály eredeti Render metódusának meghívásakor base.Render(writer) az oldalon lévő összes vezérlő megjelenik, és a jelölés a vezérlőhierarchiájuk alapján jön létre. Ezért elengedhetetlen, hogy mindketten meghívjuk base.Render(writer)a , hogy az oldal megjelenjen, és hogy a hívás base.Render(writer)előtt módosítsuk a GridView vezérlőhierarchiáját, hogy az elválasztó sorok a megjelenítés előtt hozzá legyenek adva a GridView vezérlőhierarchiájához.

A rendezési csoport fejléceinek beszúrásához először meg kell győződnünk arról, hogy a felhasználó kérte-e az adatok rendezését. Alapértelmezés szerint a GridView tartalma nincs rendezve, ezért nem kell megadnunk semmilyen csoportrendezési fejlécet.

Megjegyzés:

Ha azt szeretné, hogy a GridView egy adott oszlop szerint legyen rendezve az oldal első betöltésekor, hívja meg a GridView s Sort metódust az első oldallátogatáskor (de ne a későbbi visszaküldéseknél). Ehhez adja hozzá ezt a hívást az eseménykezelőhöz Page_Load egy if (!Page.IsPostBack) feltételen belül. A módszerrel kapcsolatos további információkért tekintse meg a Sort oktatóanyagát.

Feltételezve, hogy az adatok rendezve vannak, a következő feladatunk annak meghatározása, hogy melyik oszlop szerint rendeztük az adatokat, majd megvizsgáljuk a sorokat, és keressük az oszlop értékei közötti különbségeket. A következő kód biztosítja az adatok rendezését, és megkeresi azt az oszlopot, amely alapján az adatok rendezve vannak:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // Determine the index and HeaderText of the column that
        //the data is sorted by
        int sortColumnIndex = -1;
        string sortColumnHeaderText = string.Empty;
        for (int i = 0; i < ProductList.Columns.Count; i++)
        {
            if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
                == 0)
            {
                sortColumnIndex = i;
                sortColumnHeaderText = ProductList.Columns[i].HeaderText;
                break;
            }
        }
        // TODO: Scan the rows for differences in the sorted column�s values
}

Ha a GridView még nem lett rendezve, akkor a GridView s tulajdonsága SortExpression nem lesz beállítva. Ezért csak akkor szeretnénk hozzáadni az elválasztó sorokat, ha ennek a tulajdonságnak van valamilyen értéke. Ha igen, akkor ezután meg kell határoznunk annak az oszlopnak az indexét, amely alapján az adatokat rendeztük. Ezt úgy érhetjük el, hogy végigfutunk a GridView gyűjteményen Columns , és megkeresjük azt az oszlopot, amelynek SortExpression tulajdonsága megegyezik a GridView s SortExpression tulajdonságával. Az s index oszlop mellett megragadjuk a tulajdonságot HeaderText is, amelyet az elválasztó sorok megjelenítésénél használunk.

Annak az oszlopnak az indexével, amely alapján az adatok rendezve vannak, az utolsó lépés a GridView sorainak számbavétele. Minden sornál meg kell határoznunk, hogy a rendezett oszlop értéke eltér-e az előző sor rendezett oszlopának értékétől. Ha igen, be kell szúrnunk egy új GridViewRow példányt a vezérlőhierarchiába. Ez a következő kóddal történik:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // ... Code for finding the sorted column index removed for brevity ...
        // Reference the Table the GridView has been rendered into
        Table gridTable = (Table)ProductList.Controls[0];
        // Enumerate each TableRow, adding a sorting UI header if
        // the sorted value has changed
        string lastValue = string.Empty;
        foreach (GridViewRow gvr in ProductList.Rows)
        {
            string currentValue = gvr.Cells[sortColumnIndex].Text;
            if (lastValue.CompareTo(currentValue) != 0)
            {
                // there's been a change in value in the sorted column
                int rowIndex = gridTable.Rows.GetRowIndex(gvr);
                // Add a new sort header row
                GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
                    DataControlRowType.DataRow, DataControlRowState.Normal);
                TableCell sortCell = new TableCell();
                sortCell.ColumnSpan = ProductList.Columns.Count;
                sortCell.Text = string.Format("{0}: {1}",
                    sortColumnHeaderText, currentValue);
                sortCell.CssClass = "SortHeaderRowStyle";
                // Add sortCell to sortRow, and sortRow to gridTable
                sortRow.Cells.Add(sortCell);
                gridTable.Controls.AddAt(rowIndex, sortRow);
                // Update lastValue
                lastValue = currentValue;
            }
        }
    }
    base.Render(writer);
}

Ez a kód azzal kezdődik, hogy programozott módon hivatkozik a Table GridView vezérlőhierarchiájának gyökerében található objektumra, és létrehoz egy nevű lastValuekarakterlánc-változót. lastValue az aktuális sor rendezett oszlopának és az előző sor értékének összehasonlítására szolgál. Ezután a rendszer felsorolja a GridView s Rows gyűjteményt, és minden sorhoz a rendezett oszlop értékét tárolja a currentValue változóban.

Megjegyzés:

Az adott sor rendezett oszlopának értékének meghatározásához a cella s Text tulajdonságát használom. Ez jól működik a BoundFields esetében, de nem működik a kívánt módon a TemplateFields, CheckBoxFields stb. esetében. Hamarosan megvizsgáljuk, hogyan lehet figyelembe venni az alternatív GridView mezőket.

Ezután összehasonlítjuk az currentValue és lastValue változókat. Ha eltérnek, új elválasztó sort kell hozzáadnunk a vezérlőhierarchiához. Ez úgy érhető el, hogy meghatározzuk az objektum GridViewRow gyűjteményének indexét TableRows, új GridViewRow és példányokat TableCell hozunk létre, majd hozzáadjuk az TableCell és a GridViewRow vezérlőhierarchiához.

Vegye figyelembe, hogy az elválasztó sor egyedül TableCell úgy van formázva, hogy a GridView teljes szélességét lefedje, a SortHeaderRowStyle CSS osztállyal van formázva, és olyan tulajdonsággal Text rendelkezik, hogy a rendezési csoport nevét (például Kategória ) és a csoport értékét (például Italok) is megjeleníti. Végül lastValuecurrentValuea .

A rendezési csoport fejlécsorának SortHeaderRowStyle formázásához használt CSS osztályt meg kell adni a Styles.css fájlban. Nyugodtan használja az Ön számára tetsző stílusbeállításokat; A következőket használtam:

.SortHeaderRowStyle
{
    background-color: #c00;
    text-align: left;
    font-weight: bold;
    color: White;
}

Az aktuális kóddal a rendezési felület rendezési csoportfejléceket ad hozzá bármely kötött mező szerinti rendezéskor (lásd az 5. ábrát, amely egy képernyőképet mutat a szállító szerinti rendezéskor). Ha azonban bármely más mezőtípus (például CheckBoxField vagy TemplateField) szerint rendez, a rendezési csoport fejlécei sehol sem találhatók (lásd a 6. ábrát).

A rendezési felület tartalmazza a rendezési csoportfejléceket a BoundFields szerinti rendezéskor

5. ábra: A rendezési felület tartalmazza a csoportfejlécek rendezését a BoundFields szerinti rendezéskor (kattintson a teljes méretű kép megtekintéséhez)

A rendezési csoport fejlécei hiányoznak a CheckBoxField rendezésekor

6. ábra: Hiányoznak a rendezési csoport fejlécei a CheckBoxField rendezésekor (kattintson a teljes méretű kép megtekintéséhez)

A rendezési csoportfejlécek azért hiányoznak a CheckBoxField szerinti rendezéskor, mert a kód jelenleg csak az s TableCell tulajdonságot Text használja az egyes sorok rendezett oszlopának értékének meghatározásához. A CheckBoxFields esetében az TableCell s Text tulajdonság egy üres karakterlánc, ehelyett az érték az s TableCell gyűjteményben található Controls CheckBox webes vezérlőn keresztül érhető el.

A BoundFields mezőktől eltérő mezőtípusok kezeléséhez ki kell egészítenünk azt a kódot, amelyhez a currentValue változó hozzá van rendelve, hogy ellenőrizzük, van-e CheckBox TableCell az s Controls gyűjteményben. A használata currentValue = gvr.Cells[sortColumnIndex].Texthelyett cserélje le ezt a kódot a következőre:

string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
    if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
    {
        if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
            currentValue = "Yes";
        else
            currentValue = "No";
    }
    // ... Add other checks here if using columns with other
    //      Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
    currentValue = gvr.Cells[sortColumnIndex].Text;

Ez a kód megvizsgálja az aktuális sor rendezett oszlopát TableCell , és megállapítja, hogy vannak-e vezérlők a gyűjteményben Controls . Ha vannak, és az első vezérlő egy CheckBox, a currentValue változó értéke Igen vagy Nem lesz, a CheckBox s Checked tulajdonságától függően. Ellenkező esetben az értéket az TableCell s Text tulajdonságból vesszük. Ez a logika replikálható a GridView-ban esetlegesen létező TemplateFields rendezésének kezelésére.

A fenti kód hozzáadásával a rendezési csoportok fejlécei most már jelen vannak a megszűnt CheckBoxField szerinti rendezéskor (lásd a 7. ábrát).

A rendezési csoport fejlécei mostantól jelen vannak a CheckBoxField rendezésekor

7. ábra: A rendezési csoport fejlécei most már jelen vannak a CheckBoxField rendezésekor (kattintson a teljes méretű kép megtekintéséhez)

Megjegyzés:

Ha vannak olyan termékei, amelyek NULL adatbázis-értékei a CategoryID, SupplierIDvagy UnitPrice mezőkhöz, akkor ezek az értékek alapértelmezés szerint üres karakterláncként jelennek meg a GridView-ban, ami azt jelenti, hogy az elválasztó sor szövege az értékekkel rendelkező NULL termékeknél a következőképpen hangzik: (azaz nincs név a Kategória után: mint a Kategória: Italok esetében). Ha itt szeretne megjeleníteni egy értéket, beállíthatja a BoundFields NullDisplayText tulajdonságot a megjeleníteni kívánt szövegre, vagy hozzáadhat egy feltételes utasítást a Render metódushoz, amikor az currentValue elválasztó sor s Text tulajdonságához rendeli.

Összefoglalás

A GridView nem tartalmaz sok beépített lehetőséget a rendezési felület testreszabásához. Egy kis alacsony szintű kóddal azonban lehetőség van a GridView vezérlőhierarchiájának módosítására, hogy testreszabottabb felületet hozzon létre. Ebben az oktatóanyagban láthattuk, hogyan adhatunk hozzá rendezési csoportelválasztó sort egy rendezhető GridView-hoz, amely könnyebben azonosítja a különböző csoportokat és a csoportok határait. A testreszabott rendezési felületek további példáiért tekintse meg Scott GuthrieA Few ASP.NET 2.0 GridView rendezési tippek és trükkök blogbejegyzését .

Boldog programozást!

Tudnivalók a szerzőről

Scott Mitchell, hét ASP/ASP.NET-könyv szerzője és a 4GuysFromRolla.com alapítója, 1998 óta dolgozik a Microsoft webtechnológiáival. Scott független tanácsadóként, edzőként és íróként dolgozik. Legújabb könyve Sams Tanuld meg ASP.NET 2.0 24 óra alatt. Ő itt elérhető mitchell@4GuysFromRolla.com.